LoginSignup
6
13

More than 1 year has passed since last update.

Pandasユーザーガイド「マルチインデックス・高度な索引」(公式ドキュメント日本語訳)

Last updated at Posted at 2019-11-18

本記事は、Pandas の公式ドキュメントのUser Guide - MultiIndex / Advanced Indexingを機械翻訳した後、一部の不自然な文章を手直ししたものである。

誤訳の指摘・代訳案・質問等があればコメント欄や編集リクエストでお願いします。

Pandas公式ドキュメント日本語訳記事一覧

マルチインデックス・高度な索引

この章では、マルチインデックスによる索引およびその他の高度な索引機能について説明します。

基本的な索引に関するドキュメントは、データの索引と選択を参照してください。

代入操作において、コピーと参照のどちらが返されるのかは、場合によって異なります。これは連鎖代入chained assignmentと呼ばれ、避けるべきものです。返るのはビューかコピーかを参照してください。

より応用的な操作については、cook bookもご覧ください。

階層型インデックス(マルチインデックス)

階層型・マルチレベルのインデックスは、特に高次元のデータを扱う場合、高度なデータ分析と操作を行うのに非常に有用です。簡潔には、Series(一次元)やDataFrame(二次元)のような低次元のデータ構造に、任意の次元数のデータを格納し、操作できるようになります。

この節では、「階層型」インデックスとは具体的にどのような意味なのか、そしてそれが上記および前の章で説明したpandasの全てのインデックス機能とどのように統合されるかを示します。後にグループ化テーブルの整形とピボットテーブルについて説明するとき、分析のためのデータの構造化にどのように役立つかを説明するための重要なアプリケーションを紹介します。

より応用的なやり方については、cook bookもご覧ください。

マルチインデックス(階層型インデックス)オブジェクトの作成

MultiIndexは、一般的にpandasオブジェクトの軸ラベルを格納する通常のIndexオブジェクトが、階層的になったものです。また、それぞれがユニークなタプルの配列と考えることもできます。MultiIndexは、(MultiIndex.from_arrays()を使用して)配列のリスト、(MultiIndex.from_tuples()を使用して)タプルの配列、(MultiIndex.from_product()を使用して)イテラブルの直積、(MultiIndex.from_frame()を使用して)DataFrameから作成することができます。Indexコンストラクタは、タプルのリストが渡されるとMultiIndexを返そうとします。以下に、マルチインデックスを初期化するさまざまな方法を紹介します。

arrays = [
    ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
    ["one", "two", "one", "two", "one", "two", "one", "two"],
]


tuples = list(zip(*arrays))

tuples
# [('bar', 'one'),
#  ('bar', 'two'),
#  ('baz', 'one'),
#  ('baz', 'two'),
#  ('foo', 'one'),
#  ('foo', 'two'),
#  ('qux', 'one'),
#  ('qux', 'two')]

index = pd.MultiIndex.from_tuples(tuples, names=["first", "second"])

index
# MultiIndex([('bar', 'one'),
#             ('bar', 'two'),
#             ('baz', 'one'),
#             ('baz', 'two'),
#             ('foo', 'one'),
#             ('foo', 'two'),
#             ('qux', 'one'),
#             ('qux', 'two')],
#            names=['first', 'second'])

s = pd.Series(np.random.randn(8), index=index)

s
# first  second
# bar    one       0.469112
#        two      -0.282863
# baz    one      -1.509059
#        two      -1.135632
# foo    one       1.212112
#        two      -0.173215
# qux    one       0.119209
#        two      -1.044236
# dtype: float64

2つのイテラブルの要素のすべての組み合わせ(直積)がほしい場合は、MultiIndex.from_product()メソッドを使用するのが便利です。

iterables = [["bar", "baz", "foo", "qux"], ["one", "two"]]

pd.MultiIndex.from_product(iterables, names=["first", "second"])
# MultiIndex([('bar', 'one'),
#             ('bar', 'two'),
#             ('baz', 'one'),
#             ('baz', 'two'),
#             ('foo', 'one'),
#             ('foo', 'two'),
#             ('qux', 'one'),
#             ('qux', 'two')],
#            names=['first', 'second'])

MultiIndex.from_frame()メソッドを使用して、DataFrameから直接MultiIndexを作成することもできます。これはMultiIndex.to_frame()を補完するメソッドです。

df = pd.DataFrame(
    [["bar", "one"], ["bar", "two"], ["foo", "one"], ["foo", "two"]],
    columns=["first", "second"],
)


pd.MultiIndex.from_frame(df)
# MultiIndex([('bar', 'one'),
#             ('bar', 'two'),
#             ('foo', 'one'),
#             ('foo', 'two')],
#            names=['first', 'second'])

また便利な方法として、配列のリストをSeriesまたはDataFrameに直接渡すことで、MultiIndexを自動的に作成できます。

arrays = [
    np.array(["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"]),
    np.array(["one", "two", "one", "two", "one", "two", "one", "two"]),
]


s = pd.Series(np.random.randn(8), index=arrays)

s
# bar  one   -0.861849
#      two   -2.104569
# baz  one   -0.494929
#      two    1.071804
# foo  one    0.721555
#      two   -0.706771
# qux  one   -1.039575
#      two    0.271860
# dtype: float64

df = pd.DataFrame(np.random.randn(8, 4), index=arrays)

df
#                 0         1         2         3
# bar one -0.424972  0.567020  0.276232 -1.087401
#     two -0.673690  0.113648 -1.478427  0.524988
# baz one  0.404705  0.577046 -1.715002 -1.039268
#     two -0.370647 -1.157892 -1.344312  0.844885
# foo one  1.075770 -0.109050  1.643563 -1.469388
#     two  0.357021 -0.674600 -1.776904 -0.968914
# qux one -1.294524  0.413738  0.276662 -0.472035
#     two -0.013960 -0.362543 -0.006154 -0.923061

すべてのMultiIndexコンストラクタは、レベル自身の文字列名(ラベル)を格納するnames引数を受け取ります。名前が指定されていない場合は、Noneが割り当てられます。

df.index.names
# FrozenList([None, None])

このインデックスはpandasオブジェクトのどの方向の軸にも設定することができ、またインデックスのレベルの数は自由に決められます。

df = pd.DataFrame(np.random.randn(3, 8), index=["A", "B", "C"], columns=index)

df
# first        bar                 baz  ...       foo       qux
# second       one       two       one  ...       two       one       two
# A       0.895717  0.805244 -1.206412  ...  1.340309 -1.170299 -0.226169
# B       0.410835  0.813850  0.132003  ... -1.187678  1.130127 -1.436737
# C      -1.413681  1.607920  1.024180  ... -2.211372  0.974466 -2.006747
#
# [3 rows x 8 columns]

pd.DataFrame(np.random.randn(6, 6), index=index[:6], columns=index[:6])
# first              bar                 baz                 foo
# second             one       two       one       two       one       two
# first second
# bar   one    -0.410001 -0.078638  0.545952 -1.219217 -1.226825  0.769804
#       two    -1.281247 -0.727707 -0.121306 -0.097883  0.695775  0.341734
# baz   one     0.959726 -1.110336 -0.619976  0.149748 -0.732339  0.687738
#       two     0.176444  0.403310 -0.154951  0.301624 -2.179861 -1.369849
# foo   one    -0.954208  1.462696 -1.743161 -0.826591 -0.345352  1.314232
#       two     0.690579  0.995761  2.396780  0.014871  3.357427 -0.317441

pandasでは、コンソール出力を見やすくするために、より高いレベルのインデックスを「スパース」しています。インデックスの表示方法は、pandas.set_options()multi_sparseオプションを使用して制御できます。

with pd.option_context("display.multi_sparse", False):
    df

不可分な単一のラベルとしてタプルを用いても問題ないことは覚えておいてもよいでしょう。

pd.Series(np.random.randn(8), index=tuples)
# (bar, one)   -1.236269
# (bar, two)    0.896171
# (baz, one)   -0.487602
# (baz, two)   -0.082240
# (foo, one)   -2.182937
# (foo, two)    0.380396
# (qux, one)    0.084844
# (qux, two)    0.432390
# dtype: float64

MultiIndexがなぜ重要なのかというと、以下および以降の章で説明するように、MultiIndexを利用してグループ化・選択・形状変更操作を実行できるためです。後のセクションで見るように、自分で明示的にMultiIndexを作成することなく、階層的にインデックス付けされたデータを操作することができます。ただし、ファイルからデータを読み込む場合、データセットを準備するときに独自のMultiIndexを生成したいこともあるかもしれません。

レベルラベルの再構築

get_level_values()メソッドは、特定のレベルの各位置ラベルのベクトルを返します。

index.get_level_values(0)
# Index(['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

index.get_level_values('second')
# Index(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'], dtype='object', name='second')

マルチインデックスを使用した軸の基本的なインデックス付け

階層型インデックスの重要な機能の1つは、データ内のサブグループを識別する「partial」ラベルによってデータを選択できることです。partial選択は、通常のデータフレームの列を選択するのとまったく同じ方法で、結果の階層型インデックスのレベルを「ドロップ」します。

df["bar"]
# second       one       two
# A       0.895717  0.805244
# B       0.410835  0.813850
# C      -1.413681  1.607920

df["bar", "one"]
# A    0.895717
# B    0.410835
# C   -1.413681
# Name: (bar, one), dtype: float64

df["bar"]["one"]
# A    0.895717
# B    0.410835
# C   -1.413681
# Name: one, dtype: float64

s["qux"]
# one   -1.039575
# two    0.271860
# dtype: float64

より深いレベルで選択する方法については、階層インデックス付きの断面を参照してください。

レベル定義

MultiIndexは、実際に使用されていなくても、定義されたすべてのレベルのインデックスを保持します。インデックスをスライスするとき、これに気付くかもしれません。例えば、

df.columns.levels  # もとのマルチインデックス
# FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

df[["foo","qux"]].columns.levels  # スライスしたもの
# FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

これは、レベルの再計算を避けてスライスのパフォーマンスを向上させるための実装です。使用されているレベルのみを表示したい場合は、get_level_values()メソッドを使用します。

df[["foo", "qux"]].columns.to_numpy()
# array([('foo', 'one'), ('foo', 'two'), ('qux', 'one'), ('qux', 'two')],
#       dtype=object)

# 特定のレベルの場合
df[["foo", "qux"]].columns.get_level_values(0)
# Index(['foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

使用レベルのみでMultiIndexを再構築するには、remove_unused_levels()メソッドを使用できます。

new_mi = df[["foo", "qux"]].columns.remove_unused_levels()

new_mi.levels
# FrozenList([['foo', 'qux'], ['one', 'two']])

データのアライメントとreindexの使用

軸にMultiIndexを持つ異なるインデックスのオブジェクト間の操作は、期待どおりに機能します。データのアライメントは、タプルのインデックスと同じように機能します。

s + s[:-2]
# bar  one   -1.723698
#      two   -4.209138
# baz  one   -0.989859
#      two    2.143608
# foo  one    1.443110
#      two   -1.413542
# qux  one         NaN
#      two         NaN
# dtype: float64

s + s[::2]
# bar  one   -1.723698
#      two         NaN
# baz  one   -0.989859
#      two         NaN
# foo  one    1.443110
#      two         NaN
# qux  one   -2.079150
#      two         NaN
# dtype: float64

Series/DataFramesreindex()メソッドは、別のMultiIndex、またはタプルのリストまたは配列も受け取ることができます。

s.reindex(index[:3])
# first  second
# bar    one      -0.861849
#        two      -2.104569
# baz    one      -0.494929
# dtype: float64

s.reindex([("foo", "two"), ("bar", "one"), ("qux", "one"), ("baz", "one")])
# foo  two   -0.706771
# bar  one   -0.861849
# qux  one   -1.039575
# baz  one   -0.494929
# dtype: float64

階層型インデックスを使用した高度なインデックス作成

MultiIndex.locを用いて高度なインデックス作成を行わせることは構文的に少し難しいですが、私達はそれを実現するためにあらゆる努力をしました。一般に、マルチインデックスのキーはタプルの形式を取ります。たとえば、以下は期待どおりに機能します。

df = df.T

df
#                      A         B         C
# first second
# bar   one     0.895717  0.410835 -1.413681
#       two     0.805244  0.813850  1.607920
# baz   one    -1.206412  0.132003  1.024180
#       two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372
# qux   one    -1.170299  1.130127  0.974466
#       two    -0.226169 -1.436737 -2.006747

df.loc[("bar", "two")]
# A    0.805244
# B    0.813850
# C    1.607920
# Name: (bar, two), dtype: float64

この例の場合、df.loc['bar', 'two']でも機能しますが、この略記法は一般的に誤解を招く可能性があることに注意してください。

.locを使用して特定の列から索引を行う場合は、次のようにタプルを使用しなければなりません。

df.loc[("bar", "two"), "A"]
# 0.8052440253863785

タプルの最初の要素のみを渡す場合は、MultiIndex のすべてのレベルを指定する必要はありません。たとえば次のように「partial」インデックスを使用して、第1レベルにbarを持つすべての要素を取得できます。

df.loc["bar"]
#                A         B         C
# second
# one     0.895717  0.410835 -1.413681
# two     0.805244  0.813850  1.607920

これは、より冗長な表記であるdf.loc[('bar',),](この例ではまたdf.loc['bar',]と同等)のショートカットです。

「Partial」スライスも非常にうまく機能します。

df.loc["baz":"foo"]
#                      A         B         C
# first second
# baz   one    -1.206412  0.132003  1.024180
#       two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372

タプルのスライスを渡すことにより、値の「範囲」でスライスできます。

df.loc[("baz", "two"):("qux", "one")]
#                      A         B         C
# first second
# baz   two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372
# qux   one    -1.170299  1.130127  0.974466

df.loc[("baz", "two"):"foo"]
#                      A         B         C
# first second
# baz   two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372

インデックスの再作成と同様、ラベルまたはタプルのリストを渡すこともできます。

df.loc[[("bar", "two"), ("qux", "one")]]
#                      A         B         C
# first second
# bar   two     0.805244  0.813850  1.607920
# qux   one    -1.170299  1.130127  0.974466

pandasでは、インデックス作成の際にタプルとリストが同じように扱われないことに注意してください。タプルは1つのマルチレベルキーとして解釈されますが、リストは複数のキーを指します。言い換えると、タプルは水平に移動し(レベルの横断)、リストは垂直に移動します(レベルのスキャン)。

重要なことに、タプルのリストは複数の完全なMultiIndexキーを引きますが、リストのタプルはレベル内のいくつかの値を参照します。

s = pd.Series(
    [1, 2, 3, 4, 5, 6],
    index=pd.MultiIndex.from_product([["A", "B"], ["c", "d", "e"]]),
)


s.loc[[("A", "c"), ("B", "d")]]  # タプルのリスト
# A  c    1
# B  d    5
# dtype: int64

s.loc[(["A", "B"], ["c", "d"])]  # リストのタプル
# A  c    1
#    d    2
# B  c    4
#    d    5
# dtype: int64

スライサーの使用

複数のインデクサを与えることにより、MultiIndexをスライスできます。

ラベルによる選択で見た、スライス・ラベルのリスト・ラベル・真偽値配列のどのセレクターも、同様に使用することができます。

slice(None)を使用して、そのレベルのすべての要素を選択できます。すべてのより深いレベルを指定する必要はありません。それらはslice(None)によって暗示されます。

他と同様、これはラベルのインデックス付けであるため、スライサーの両側が含まれます。

.locでは、すべての軸を指定する必要があります。つまり、indexcolumnsの両方のインデクサを渡してください。渡されたインデクサが、行のMultiIndexなどではなく、両方の軸のインデックスとして誤って解釈される可能性がある、あいまいなケースがいくつかあります。
以下のように記述してください。

df.loc[(slice("A1", "A3"), ...), :]  # noqa: E999

以下のようには記述しないでください。

df.loc[(slice("A1", "A3"), ...)]  # noqa: E999
def mklbl(prefix, n):
    return ["%s%s" % (prefix, i) for i in range(n)]


miindex = pd.MultiIndex.from_product(
    [mklbl("A", 4), mklbl("B", 2), mklbl("C", 4), mklbl("D", 2)]
)


micolumns = pd.MultiIndex.from_tuples(
    [("a", "foo"), ("a", "bar"), ("b", "foo"), ("b", "bah")], names=["lvl0", "lvl1"]
)


dfmi = (
    pd.DataFrame(
        np.arange(len(miindex) * len(micolumns)).reshape(
            (len(miindex), len(micolumns))
        ),
        index=miindex,
        columns=micolumns,
    )
    .sort_index()
    .sort_index(axis=1)
)


dfmi
# lvl0           a         b
# lvl1         bar  foo  bah  foo
# A0 B0 C0 D0    1    0    3    2
#          D1    5    4    7    6
#       C1 D0    9    8   11   10
#          D1   13   12   15   14
#       C2 D0   17   16   19   18
# ...          ...  ...  ...  ...
# A3 B1 C1 D1  237  236  239  238
#       C2 D0  241  240  243  242
#          D1  245  244  247  246
#       C3 D0  249  248  251  250
#          D1  253  252  255  254
#
# [64 rows x 4 columns]

スライス・リスト・ラベルを使用した基本的なマルチインデックススライシング。

dfmi.loc[(slice("A1", "A3"), slice(None), ["C1", "C3"]), :]
# lvl0           a         b
# lvl1         bar  foo  bah  foo
# A1 B0 C1 D0   73   72   75   74
#          D1   77   76   79   78
#       C3 D0   89   88   91   90
#          D1   93   92   95   94
#    B1 C1 D0  105  104  107  106
# ...          ...  ...  ...  ...
# A3 B0 C3 D1  221  220  223  222
#    B1 C1 D0  233  232  235  234
#          D1  237  236  239  238
#       C3 D0  249  248  251  250
#          D1  253  252  255  254
#
# [24 rows x 4 columns]

Pandas.IndexSliceを使用して、slice(None)ではなく:を使用したより自然な構文を用いることができます。

idx = pd.IndexSlice

dfmi.loc[idx[:, :, ["C1", "C3"]], idx[:, "foo"]]
# lvl0           a    b
# lvl1         foo  foo
# A0 B0 C1 D0    8   10
#          D1   12   14
#       C3 D0   24   26
#          D1   28   30
#    B1 C1 D0   40   42
# ...          ...  ...
# A3 B0 C3 D1  220  222
#    B1 C1 D0  232  234
#          D1  236  238
#       C3 D0  248  250
#          D1  252  254
#
# [32 rows x 2 columns]

このメソッドを使用すると、非常に複雑な選択を複数の軸で同時に実行することができます。

dfmi.loc["A1", (slice(None), "foo")]
# lvl0        a    b
# lvl1      foo  foo
# B0 C0 D0   64   66
#       D1   68   70
#    C1 D0   72   74
#       D1   76   78
#    C2 D0   80   82
# ...       ...  ...
# B1 C1 D1  108  110
#    C2 D0  112  114
#       D1  116  118
#    C3 D0  120  122
#       D1  124  126
#
# [16 rows x 2 columns]

dfmi.loc[idx[:, :, ["C1", "C3"]], idx[:, "foo"]]
# lvl0           a    b
# lvl1         foo  foo
# A0 B0 C1 D0    8   10
#          D1   12   14
#       C3 D0   24   26
#          D1   28   30
#    B1 C1 D0   40   42
# ...          ...  ...
# A3 B0 C3 D1  220  222
#    B1 C1 D0  232  234
#          D1  236  238
#       C3 D0  248  250
#          D1  252  254
#
# [32 rows x 2 columns]

ブールインデクサを使用すると、に関連する選択を提供できます。

mask = dfmi[("a", "foo")] > 200

dfmi.loc[idx[mask, :, ["C1", "C3"]], idx[:, "foo"]]
# lvl0           a    b
# lvl1         foo  foo
# A3 B0 C1 D1  204  206
#       C3 D0  216  218
#          D1  220  222
#    B1 C1 D0  232  234
#          D1  236  238
#       C3 D0  248  250
#          D1  252  254

.locaxis引数を指定して、単一の軸で渡されたスライサーを解釈することもできます。

dfmi.loc(axis=0)[:, :, ["C1", "C3"]]
# lvl0           a         b
# lvl1         bar  foo  bah  foo
# A0 B0 C1 D0    9    8   11   10
#          D1   13   12   15   14
#       C3 D0   25   24   27   26
#          D1   29   28   31   30
#    B1 C1 D0   41   40   43   42
# ...          ...  ...  ...  ...
# A3 B0 C3 D1  221  220  223  222
#    B1 C1 D0  233  232  235  234
#          D1  237  236  239  238
#       C3 D0  249  248  251  250
#          D1  253  252  255  254
#
# [32 rows x 4 columns]

さらに、次の方法を使用して値を代入できます。

df2 = dfmi.copy()

df2.loc(axis=0)[:, :, ["C1", "C3"]] = -10

df2
# lvl0           a         b
# lvl1         bar  foo  bah  foo
# A0 B0 C0 D0    1    0    3    2
#          D1    5    4    7    6
#       C1 D0  -10  -10  -10  -10
#          D1  -10  -10  -10  -10
#       C2 D0   17   16   19   18
# ...          ...  ...  ...  ...
# A3 B1 C1 D1  -10  -10  -10  -10
#       C2 D0  241  240  243  242
#          D1  245  244  247  246
#       C3 D0  -10  -10  -10  -10
#          D1  -10  -10  -10  -10
#
# [64 rows x 4 columns]

整列可能なオブジェクトの右辺も使用できます。

df2 = dfmi.copy()

df2.loc[idx[:, :, ["C1", "C3"]], :] = df2 * 1000

df2
# lvl0              a               b
# lvl1            bar     foo     bah     foo
# A0 B0 C0 D0       1       0       3       2
#          D1       5       4       7       6
#       C1 D0    9000    8000   11000   10000
#          D1   13000   12000   15000   14000
#       C2 D0      17      16      19      18
# ...             ...     ...     ...     ...
# A3 B1 C1 D1  237000  236000  239000  238000
#       C2 D0     241     240     243     242
#          D1     245     244     247     246
#       C3 D0  249000  248000  251000  250000
#          D1  253000  252000  255000  254000
#
# [64 rows x 4 columns]

Cross-section

DataFramexs()メソッドは、さらにMultiIndexの特定のレベルでデータを選択しやすくするために level 引数を取ります。

df
#                      A         B         C
# first second
# bar   one     0.895717  0.410835 -1.413681
#       two     0.805244  0.813850  1.607920
# baz   one    -1.206412  0.132003  1.024180
#       two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372
# qux   one    -1.170299  1.130127  0.974466
#       two    -0.226169 -1.436737 -2.006747

df.xs("one", level="second")
#               A         B         C
# first
# bar    0.895717  0.410835 -1.413681
# baz   -1.206412  0.132003  1.024180
# foo    1.431256 -0.076467  0.875906
# qux   -1.170299  1.130127  0.974466
# スライスの使用
df.loc[(slice(None), "one"), :]
#                      A         B         C
# first second
# bar   one     0.895717  0.410835 -1.413681
# baz   one    -1.206412  0.132003  1.024180
# foo   one     1.431256 -0.076467  0.875906
# qux   one    -1.170299  1.130127  0.974466

axis引数を指定することにより、xsで列を選択することもできます。

df = df.T

df.xs("one", level="second", axis=1)
# first       bar       baz       foo       qux
# A      0.895717 -1.206412  1.431256 -1.170299
# B      0.410835  0.132003 -0.076467  1.130127
# C     -1.413681  1.024180  0.875906  0.974466
# スライスの使用
df.loc[:, (slice(None), "one")]
# first        bar       baz       foo       qux
# second       one       one       one       one
# A       0.895717 -1.206412  1.431256 -1.170299
# B       0.410835  0.132003 -0.076467  1.130127
# C      -1.413681  1.024180  0.875906  0.974466

xsでは、複数のキーを使用した選択も可能です。

df.xs(("one", "bar"), level=("second", "first"), axis=1)
# first        bar
# second       one
# A       0.895717
# B       0.410835
# C      -1.413681
# スライスの使用
df.loc[:, ("bar", "one")]
# A    0.895717
# B    0.410835
# C   -1.413681
# Name: (bar, one), dtype: float64

drop_level=Falsexsに渡すと、選択したレベルを保持できます。

df.xs("one", level="second", axis=1, drop_level=False)
# first        bar       baz       foo       qux
# second       one       one       one       one
# A       0.895717 -1.206412  1.431256 -1.170299
# B       0.410835  0.132003 -0.076467  1.130127
# C      -1.413681  1.024180  0.875906  0.974466

上の結果をdrop_level=True(デフォルト値)の場合と比べてみてください。

df.xs("one", level="second", axis=1, drop_level=True)
# first       bar       baz       foo       qux
# A      0.895717 -1.206412  1.431256 -1.170299
# B      0.410835  0.132003 -0.076467  1.130127
# C     -1.413681  1.024180  0.875906  0.974466

高度な再インデックス付けとアライメント

pandas オブジェクトのreindex()およびalign()メソッドでlevel引数を使用すると、レベル全体に値をブロードキャストするのに役立ちます。例えば:

midx = pd.MultiIndex(
    levels=[["zero", "one"], ["x", "y"]], codes=[[1, 1, 0, 0], [1, 0, 1, 0]]
)


df = pd.DataFrame(np.random.randn(4, 2), index=midx)

df
#                0         1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

df2 = df.groupby(level=0).mean()

df2
#              0         1
# one   1.060074 -0.109716
# zero  1.271532  0.713416

df2.reindex(df.index, level=0)
#                0         1
# one  y  1.060074 -0.109716
#      x  1.060074 -0.109716
# zero y  1.271532  0.713416
#      x  1.271532  0.713416

# アラインメント
df_aligned, df2_aligned = df.align(df2, level=0)

df_aligned
#                0         1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

df2_aligned
#                0         1
# one  y  1.060074 -0.109716
#      x  1.060074 -0.109716
# zero y  1.271532  0.713416
#      x  1.271532  0.713416

swaplevelによるレベルの順序交換

swaplevel()メソッドは、2 つのレベルの順序を交換することができます。

df[:5]
#                0         1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

df[:5].swaplevel(0, 1, axis=0)
#                0         1
# y one   1.519970 -0.493662
# x one   0.600178  0.274230
# y zero  0.132885 -0.023688
# x zero  2.410179  1.450520

reorder_levelsによるレベルの並べ替え

reorder_levels()メソッドは、swaplevelメソッドを一般化して、階層的インデックスのレベルを1ステップで置換できるようにします。

df[:5].reorder_levels([1, 0], axis=0)
#                0         1
# y one   1.519970 -0.493662
# x one   0.600178  0.274230
# y zero  0.132885 -0.023688
# x zero  2.410179  1.450520

IndexあるいはMultiIndexの名前の変更

通常DataFrameの列名を変更するために使用されるrename()メソッドは、MultiIndexのラベルの名前を変更することもできます。 renamecolumns引数には名前を変更する列のみを含む辞書を指定できます。

df.rename(columns={0: "col0", 1: "col1"})
#             col0      col1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

このメソッドは、DataFrameのメインインデックスの特定のラベルの名前を変更するためにも使用できます。

df.rename(index={"one": "two", "y": "z"})
#                0         1
# two  z  1.519970 -0.493662
#      x  0.600178  0.274230
# zero z  0.132885 -0.023688
#      x  2.410179  1.450520

rename_axis()メソッドは、IndexまたはMultiIndexの名前(name属性)を変更します。特に、MultiIndexのレベルの名前を指定できますが、これは後でreset_index()を使用して値をMultiIndexから通常の列に移動する場合に便利です。

df.rename_axis(index=["abc", "def"])
#                  0         1
# abc  def
# one  y    1.519970 -0.493662
#      x    0.600178  0.274230
# zero y    0.132885 -0.023688
#      x    2.410179  1.450520

DataFrameの列はインデックスであるため、rename_axiscolumns引数とともに使用すると、そのインデックスの名前が変更されることに注意してください。

df.rename_axis(columns="Cols").columns
# RangeIndex(start=0, stop=2, step=1, name='Cols')

Renameおよびrename_axisは、ラベル/名前を新しい値にマッピングするための、辞書・Series・マッピング関数の指定をサポートしています。

DataFrame経由ではなく、Indexオブジェクトを直接操作する場合、Index.set_names()を使用して名前を変更できます。

mi = pd.MultiIndex.from_product([[1, 2], ["a", "b"]], names=["x", "y"])

mi.names
# FrozenList(['x', 'y'])

mi2 = mi.rename("new name", level=0)

mi2
# MultiIndex([(1, 'a'),
#             (1, 'b'),
#             (2, 'a'),
#             (2, 'b')],
#            names=['new name', 'y'])

レベルを介してマルチインデックスの名前を設定することはできません。

mi.levels[0].name = "name via level"
# ---------------------------------------------------------------------------
# RuntimeError                              Traceback (most recent call last)
# Input In [100], in <module>
# ----> 1 mi.levels[0].name = "name via level"
#
# File ~/work/pandas/pandas/pandas/core/indexes/base.py:1728, in Index.name(self, value)
#    1724 @name.setter
#    1725 def name(self, value: Hashable):
#    1726     if self._no_setting_name:
#    1727         # Used in MultiIndex.levels to avoid silently ignoring name updates.
# -> 1728         raise RuntimeError(
#    1729             "Cannot set name on a level of a MultiIndex. Use "
#    1730             "'MultiIndex.set_names' instead."
#    1731         )
#    1732     maybe_extract_name(value, None, type(self))
#    1733     self._name = value
#
# RuntimeError: Cannot set name on a level of a MultiIndex. Use 'MultiIndex.set_names' instead.

代わりにIndex.set_names()を使用してください。

MultiIndexのソート

MultiIndexが適用されたオブジェクトを効果的にインデックスおよびスライスするには、ソートする必要があります。他のインデックスと同様に、sort_index()を使用できます。

import random

random.shuffle(tuples)

s = pd.Series(np.random.randn(8), index=pd.MultiIndex.from_tuples(tuples))

s
# baz  two    0.206053
# qux  two   -0.251905
# foo  two   -2.213588
# bar  one    1.063327
# qux  one    1.266143
# foo  one    0.299368
# baz  one   -0.863838
# bar  two    0.408204
# dtype: float64

s.sort_index()
# bar  one    1.063327
#      two    0.408204
# baz  one   -0.863838
#      two    0.206053
# foo  one    0.299368
#      two   -2.213588
# qux  one    1.266143
#      two   -0.251905
# dtype: float64

s.sort_index(level=0)
# bar  one    1.063327
#      two    0.408204
# baz  one   -0.863838
#      two    0.206053
# foo  one    0.299368
#      two   -2.213588
# qux  one    1.266143
#      two   -0.251905
# dtype: float64

s.sort_index(level=1)
# bar  one    1.063327
# baz  one   -0.863838
# foo  one    0.299368
# qux  one    1.266143
# bar  two    0.408204
# baz  two    0.206053
# foo  two   -2.213588
# qux  two   -0.251905
# dtype: float64

MultiIndexのレベルに名前が付けられている場合は、レベル名をsort_indexに渡すことができます。

s.index.set_names(["L1", "L2"], inplace=True)

s.sort_index(level="L1")
# L1   L2
# bar  one    1.063327
#      two    0.408204
# baz  one   -0.863838
#      two    0.206053
# foo  one    0.299368
#      two   -2.213588
# qux  one    1.266143
#      two   -0.251905
# dtype: float64

s.sort_index(level="L2")
# L1   L2
# bar  one    1.063327
# baz  one   -0.863838
# foo  one    0.299368
# qux  one    1.266143
# bar  two    0.408204
# baz  two    0.206053
# foo  two   -2.213588
# qux  two   -0.251905
# dtype: float64

高次元のオブジェクトでは、MultiIndexがある場合、インデックス以外の軸でもレベルでソートできます。

df.T.sort_index(level=1, axis=1)
#         one      zero       one      zero
#           x         x         y         y
# 0  0.600178  2.410179  1.519970  0.132885
# 1  0.274230  1.450520 -0.493662 -0.023688

インデックス付けは、データが並べ替えられていない場合でも機能しますが、かなり非効率的です(そしてPerformanceWarningが表示されます)。また、ビューではなくデータのコピーを返します。

dfm = pd.DataFrame(
    {"jim": [0, 0, 1, 1], "joe": ["x", "x", "z", "y"], "jolie": np.random.rand(4)}
)


dfm = dfm.set_index(["jim", "joe"])

dfm
#             jolie
# jim joe
# 0   x    0.490671
#     x    0.120248
# 1   z    0.537020
#     y    0.110968
dfm.loc[(1, 'z')]
# PerformanceWarning: indexing past lexsort depth may impact performance.

#            jolie
# jim joe
# 1   z    0.64094

さらに、完全にレックスソートされていない場合に索引すると、以下のようなエラーが発生することがあります。

dfm.loc[(0, 'y'):(1, 'z')]
# UnsortedIndexError: 'Key length (2) was greater than MultiIndex lexsort depth (1)'

MultiIndexis_monotonic_increasing()メソッドは、インデックスがソートされているかどうかを表示します。

dfm.index.is_monotonic_increasing
# False
dfm = dfm.sort_index()

dfm
#             jolie
# jim joe
# 0   x    0.490671
#     x    0.120248
# 1   y    0.110968
#     z    0.537020

dfm.index.is_monotonic_increasing
# True

これで、期待通りに選択が機能するようになりました。

dfm.loc[(0, "y"):(1, "z")]
#             jolie
# jim joe
# 1   y    0.110968
#     z    0.537020

takeメソッド

NumPyのndarraysと同様に、pandasのIndexSeriesDataFrameは、特定のインデックスで特定の軸に沿って要素を取得するtake()メソッドも提供します。指定されたインデックスは、リストまたは整数インデックス位置のndarrayでなければなりません。takeは、オブジェクトの末尾からの相対位置として負の整数も受け取ることができます。

index = pd.Index(np.random.randint(0, 1000, 10))

index
# Int64Index([214, 502, 712, 567, 786, 175, 993, 133, 758, 329], dtype='int64')

positions = [0, 9, 3]

index[positions]
# Int64Index([214, 329, 567], dtype='int64')

index.take(positions)
# Int64Index([214, 329, 567], dtype='int64')

ser = pd.Series(np.random.randn(10))

ser.iloc[positions]
# 0   -0.179666
# 9    1.824375
# 3    0.392149
# dtype: float64

ser.take(positions)
# 0   -0.179666
# 9    1.824375
# 3    0.392149
# dtype: float64

DataFrameの場合、指定されたインデックスは、行または列の位置を指定する一次元のリストまたはndarrayである必要があります。

frm = pd.DataFrame(np.random.randn(5, 3))

frm.take([1, 4, 3])
#           0         1         2
# 1 -1.237881  0.106854 -1.276829
# 4  0.629675 -1.425966  1.857704
# 3  0.979542 -1.633678  0.615855

frm.take([0, 2], axis=1)
#           0         2
# 0  0.595974  0.601544
# 1 -1.237881 -1.276829
# 2 -0.767101  1.499591
# 3  0.979542  0.615855
# 4  0.629675  1.857704

pandasオブジェクトのtakeメソッドは、ブールインデックスで動作するように意図されておらず、予期しない結果を返す可能性があることに注意してください。

arr = np.random.randn(10)

arr.take([False, False, True, True])
# array([-1.1935, -1.1935,  0.6775,  0.6775])

arr[[0, 1]]
# array([-1.1935,  0.6775])

ser = pd.Series(np.random.randn(10))

ser.take([False, False, True, True])
# 0    0.233141
# 0    0.233141
# 1   -0.223540
# 1   -0.223540
# dtype: float64

ser.iloc[[0, 1]]
# 0    0.233141
# 1   -0.223540
# dtype: float64

最後に、パフォーマンスについて少し触れておくと、takeメソッドはより狭い範囲の入力を処理するため、ファンシーインデックスよりもはるかに高速なパフォーマンスを提供できます。

arr = np.random.randn(10000, 5)

indexer = np.arange(10000)

random.shuffle(indexer)

%timeit arr[indexer]
%timeit arr.take(indexer, axis=0)
# 186 us +- 1.53 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
# 56.1 us +- 5.76 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
ser = pd.Series(arr[:, 0])

%timeit ser.iloc[indexer]
%timeit ser.take(indexer)
# 103 us +- 7.31 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
# 83.9 us +- 6.52 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)

インデックスの型

ここまででMultiIndexについてかなり広範囲に説明しました。DatetimeIndexおよびPeriodIndexに関するドキュメントはこちらTimedeltaIndexに関するドキュメントはこちらを参照してください。

以下のサブセクションでは、他のいくつかのインデックスタイプを強調します。

CategoricalIndex

CategoricalIndexは、重複のある索引をサポートするのに役立つインデックスです。これは、Categoricalを囲むコンテナであり、多数の重複した要素を含むインデックスの効率的なインデックス作成と保存を可能にします。

from pandas.api.types import CategoricalDtype

df = pd.DataFrame({"A": np.arange(6), "B": list("aabbca")})

df["B"] = df["B"].astype(CategoricalDtype(list("cab")))

df
#    A  B
# 0  0  a
# 1  1  a
# 2  2  b
# 3  3  b
# 4  4  c
# 5  5  a

df.dtypes
# A       int64
# B    category
# dtype: object

df["B"].cat.categories
# Index(['c', 'a', 'b'], dtype='object')

インデックスを設定すると、CategoricalIndexが作成されます。

df2 = df.set_index("B")

df2.index
# CategoricalIndex(['a', 'a', 'b', 'b', 'c', 'a'], categories=['c', 'a', 'b'], ordered=False, dtype='category', name='B')

__getitem__/.iloc/.locを使用した索引は、Indexと同様に機能します。インデクサはカテゴリに属していなければなりません。そうでない場合はKeyErrorが発生します。

df2.loc["a"]
#    A
# B
# a  0
# a  1
# a  5

CategoricalIndexは、索引後も保持されます

df2.loc["a"].index
# CategoricalIndex(['a', 'a', 'a'], categories=['c', 'a', 'b'], ordered=False, dtype='category', name='B')

インデックスを並べ替えると、カテゴリの順序で並べ替えられます(CategoricalDtype(list('cab'))でインデックスを作成したため、cabの順に並び替えられます)。

df2.sort_index()
#    A
# B
# c  4
# a  0
# a  1
# a  5
# b  2
# b  3

インデックスに対するGroupby操作でも同様に、インデックスの性質が保持されます。

df2.groupby(level=0).sum()
#    A
# B
# c  4
# a  6
# b  5

df2.groupby(level=0).sum().index
# CategoricalIndex(['c', 'a', 'b'], categories=['c', 'a', 'b'], ordered=False, dtype='category', name='B')

インデックスの再作成操作は、渡されたインデクサの型に基づいて結果のインデックスを返します。リストを渡すと、普通のIndexが返されます。Categoricalを渡すと、その渡されたCategoricaldtypeのカテゴリに従ってインデックス付けされたCategoricalIndexを返します。これにより、pandasのインデックスを再作成する方法と同様に、カテゴリに存在しない値でもこれらを任意にインデックス化できます。

df3 = pd.DataFrame(
    {"A": np.arange(3), "B": pd.Series(list("abc")).astype("category")}
)


df3 = df3.set_index("B")

df3
#    A
# B
# a  0
# b  1
# c  2
df3.reindex(["a", "e"])
#      A
# B
# a  0.0
# e  NaN

df3.reindex(["a", "e"]).index
# Index(['a', 'e'], dtype='object', name='B')

df3.reindex(pd.Categorical(["a", "e"], categories=list("abe")))
#      A
# B
# a  0.0
# e  NaN

df3.reindex(pd.Categorical(["a", "e"], categories=list("abe"))).index
# CategoricalIndex(['a', 'e'], categories=['a', 'b', 'e'], ordered=False, dtype='category', name='B')

CategoricalIndexに対して再形成や比較といった操作を行う場合は、同じカテゴリを持っている必要があります。そうでない場合はTypeErrorが発生します。

df4 = pd.DataFrame({"A": np.arange(2), "B": list("ba")})

df4["B"] = df4["B"].astype(CategoricalDtype(list("ab")))

df4 = df4.set_index("B")

df4.index
# CategoricalIndex(['b', 'a'], categories=['a', 'b'], ordered=False, dtype='category', name='B')

df5 = pd.DataFrame({"A": np.arange(2), "B": list("bc")})

df5["B"] = df5["B"].astype(CategoricalDtype(list("bc")))

df5 = df5.set_index("B")

df5.index
# CategoricalIndex(['b', 'c'], categories=['b', 'c'], ordered=False, dtype='category', name='B')
pd.concat([df4, df5])
# TypeError: categories must match existing categories when appending

Int64Index と RangeIndex

バージョン1.4.0から非推奨:pandasのバージョン2.0では、Int64IndexFloat64IndexUInt64Indexの代わりにIndexが数値型のデフォルトインデックス型となり、したがってこれらのインデックス型は非推奨であり、将来のバージョンで削除される予定です。RangeIndexは整数インデックスの最適化されたバージョンであるため、削除されることはありません。

Int64Indexは、pandasのインデックスの基礎中の基礎です。これは、順序付けられたスライス可能なセットを実装する不変の配列です。

RangeIndexは、すべてのNDFrameオブジェクトのデフォルトインデックスを提供するInt64Indexのサブクラスです。RangeIndexは、Int64Indexが単調な順序セットを表すために最適化されたバージョンです。これらはPythonのrange型に類似しています。

Float64Index

バージョン1.4.0から非推奨:pandasのバージョン2.0では、Int64IndexFloat64IndexUInt64Indexの代わりにIndexが数値型のデフォルトインデックス型となり、したがってこれらのインデックス型は非推奨であり、将来のバージョンで削除される予定です。RangeIndexは整数インデックスの最適化されたバージョンであるため、削除されることはありません。

既定では、Float64Indexは、インデックス作成で浮動少数、または整数と浮動小数の混合値を渡すときに自動的に作成されます。これにより、スカラーインデックスとスライシングの[]ixlocをまったく同じように動作させる純粋なラベルベースのスライシングパラダイムが可能になります。

indexf = pd.Index([1.5, 2, 3, 4.5, 5])

indexf
# Float64Index([1.5, 2.0, 3.0, 4.5, 5.0], dtype='float64')

sf = pd.Series(range(5), index=indexf)

sf
# 1.5    0
# 2.0    1
# 3.0    2
# 4.5    3
# 5.0    4
# dtype: int64

[].locのスカラー選択は、常にラベルベースになります。整数の指定は、等しいfloatインデックスに一致します(たとえば、33.0と同等です)。

sf[3]
# 2

sf[3.0]
# 2

sf.loc[3]
# 2

sf.loc[3.0]
# 2

唯一の位置インデックスはiloc経由です。

sf.iloc[3]
# 3

見つからないスカラーインデックスはKeyErrorを送出します。スライスは、主に[]ixlocを使用する場合はインデックスの値に基づいており、ilocを使用する場合は常に位置に基づきます。例外は、スライスが真偽値の場合です。この場合、常に位置に基づきます。

sf[2:4]
# 2.0    1
# 3.0    2
# dtype: int64

sf.loc[2:4]
# 2.0    1
# 3.0    2
# dtype: int64

sf.iloc[2:4]
# 3.0    2
# 4.5    3
# dtype: int64

floatインデックスでは、浮動小数を使用したスライスを使うことができます。

sf[2.1:4.6]
# 3.0    2
# 4.5    3
# dtype: int64

sf.loc[2.1:4.6]
# 3.0    2
# 4.5    3
# dtype: int64

floatインデックスでない場合、浮動小数を使用したスライスはTypeErrorを発生させます。

pd.Series(range(5))[3.5]
# TypeError: the label [3.5] is not a proper indexer for this index type (Int64Index)

pd.Series(range(5))[3.5:4.5]
# TypeError: the slice start [3.5] is not a proper indexer for this index type (Int64Index)

このタイプのインデックスを使用する典型的な使用例を紹介します。データがfloatとして記録されている、不規則なtimedeltaライクのインデックススキームを想像してください。これは、例えばミリ秒のオフセットかもしれません。

dfir = pd.concat(
    [
        pd.DataFrame(
            np.random.randn(5, 2), index=np.arange(5) * 250.0, columns=list("AB")
        ),
        pd.DataFrame(
            np.random.randn(6, 2),
            index=np.arange(4, 10) * 250.1,
            columns=list("AB"),
        ),
    ]
)


dfir
#                A         B
# 0.0    -0.435772 -1.188928
# 250.0  -0.808286 -0.284634
# 500.0  -1.815703  1.347213
# 750.0  -0.243487  0.514704
# 1000.0  1.162969 -0.287725
# 1000.4 -0.179734  0.993962
# 1250.5 -0.212673  0.909872
# 1500.6 -0.733333 -0.349893
# 1750.7  0.456434 -0.306735
# 2000.8  0.553396  0.166221
# 2250.9 -0.101684 -0.734907

選択操作は、すべての選択演算子に対して常に値ベースで機能します。

dfir[0:1000.4]
#                A         B
# 0.0    -0.435772 -1.188928
# 250.0  -0.808286 -0.284634
# 500.0  -1.815703  1.347213
# 750.0  -0.243487  0.514704
# 1000.0  1.162969 -0.287725
# 1000.4 -0.179734  0.993962

dfir.loc[0:1001, "A"]
# 0.0      -0.435772
# 250.0    -0.808286
# 500.0    -1.815703
# 750.0    -0.243487
# 1000.0    1.162969
# 1000.4   -0.179734
# Name: A, dtype: float64

dfir.loc[1000.4]
# A   -0.179734
# B    0.993962
# Name: 1000.4, dtype: float64

次のようにしてデータの最初の1秒(1000ミリ秒)を取得できます。

dfir[0:1000]
#                A         B
# 0.0    -0.435772 -1.188928
# 250.0  -0.808286 -0.284634
# 500.0  -1.815703  1.347213
# 750.0  -0.243487  0.514704
# 1000.0  1.162969 -0.287725

整数位置ベースの選択が必要な場合は、ilocを使用してください。

dfir.iloc[0:5]
#                A         B
# 0.0    -0.435772 -1.188928
# 250.0  -0.808286 -0.284634
# 500.0  -1.815703  1.347213
# 750.0  -0.243487  0.514704
# 1000.0  1.162969 -0.287725

IntervalIndex

IntervalIndexは、独自のdtypeであるIntervalDtypeおよびIntervalスカラー型とともに、pandasでインターバル表記の第一級にサポートします。

IntervalIndexはいくつかの一意のインデックスを作成でき、cut()およびqcut()のカテゴリの戻り値の型としても使用されます。

IntervalIndexによる索引

IntervalIndexは、SeriesおよびDataFrameでインデックスとして使用できます。

df = pd.DataFrame(
    {"A": [1, 2, 3, 4]}, index=pd.IntervalIndex.from_breaks([0, 1, 2, 3, 4])
)


df
#         A
# (0, 1]  1
# (1, 2]  2
# (2, 3]  3
# (3, 4]  4

インターバルの端に沿った.locを介したラベルベースのインデックスは、期待どおりに機能し、その特定のインターバルを選択します。

df.loc[2]
# A    2
# Name: (1, 2], dtype: int64

df.loc[[2, 3]]
#         A
# (1, 2]  2
# (2, 3]  3

インターバル内に含まれるラベルを選択すると、そのインターバルごと選択されます。

df.loc[2.5]
# A    3
# Name: (2, 3], dtype: int64

df.loc[[2.5, 3.5]]
#         A
# (2, 3]  3
# (3, 4]  4

Intervalを使用して選択すると、完全に一致するもののみが返されます(pandas 0.25.0以降)。

df.loc[pd.Interval(1, 2)]
# A    2
# Name: (1, 2], dtype: int64

IntervalIndexに正確に含まれていないIntervalを選択しようとすると、KeyErrorが発生します。

df.loc[pd.Interval(0.5, 2.5)]
# ---------------------------------------------------------------------------
# KeyError: Interval(0.5, 2.5, closed='right')

特定のIntervalに重なるすべてのIntervalsを選択するには、overlaps()メソッドを使用してブールインデクサを作成します。

idxr = df.index.overlaps(pd.Interval(0.5, 2.5))

idxr
# array([ True,  True,  True, False])

df[idxr]
#         A
# (0, 1]  1
# (1, 2]  2
# (2, 3]  3

cutおよびqcutを使用したデータのビンニング

cut()qcut()は両方ともCategoricalオブジェクトを返し、それらが作成するビンは.categories属性にIntervalIndexとして保存されます。

c = pd.cut(range(4), bins=2)

c
# [(-0.003, 1.5], (-0.003, 1.5], (1.5, 3.0], (1.5, 3.0]]
# Categories (2, interval[float64, right]): [(-0.003, 1.5] < (1.5, 3.0]]

c.categories
# IntervalIndex([(-0.003, 1.5], (1.5, 3.0]], dtype='interval[float64, right]')

cut()は、bins引数にIntervalIndexを渡すこともできます。これにより、便利なpandasイディオムが可能になります。最初に、いくつかのデータとbinsを固定数に設定してcut()を呼び出し、ビンを生成します。次に、その.categoriesの値を、続いて呼び出したcut()bins引数に渡すことで、新しいデータを同じビンでビニングすることができます。

pd.cut([0, 3, 5, 1], bins=c.categories)
# [(-0.003, 1.5], (1.5, 3.0], NaN, (-0.003, 1.5]]
# Categories (2, interval[float64, right]): [(-0.003, 1.5] < (1.5, 3.0]]

すべてのビンの外側にある値には、NaN値が割り当てられます。

インターバルの範囲の作成

もし、一定の間隔のインターバルが必要な場合、interval_range()関数を使用して、startendperiodsのさまざまな組み合わせを使用してIntervalIndexを作成できます。interval_rangeのデフォルトの周期は、数値のインターバルの場合は1、datetimeライクなインターバルの場合は暦日です。

pd.interval_range(start=0, end=5)
# IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]], dtype='interval[int64, right]')

pd.interval_range(start=pd.Timestamp("2017-01-01"), periods=4)
# IntervalIndex([(2017-01-01, 2017-01-02], (2017-01-02, 2017-01-03], (2017-01-03, 2017-01-04], (2017-01-04, 2017-01-05]], dtype='interval[datetime64[ns], right]')

pd.interval_range(end=pd.Timedelta("3 days"), periods=3)
# IntervalIndex([(0 days 00:00:00, 1 days 00:00:00], (1 days 00:00:00, 2 days 00:00:00], (2 days 00:00:00, 3 days 00:00:00]], dtype='interval[timedelta64[ns], right]')

freq引数は、デフォルト以外の周期を指定するために使用でき、datetimeライクなインターバルのではさまざまな周期エイリアスを利用できます。

pd.interval_range(start=0, periods=5, freq=1.5)
# IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0], (6.0, 7.5]], dtype='interval[float64, right]')

pd.interval_range(start=pd.Timestamp("2017-01-01"), periods=4, freq="W")
# IntervalIndex([(2017-01-01, 2017-01-08], (2017-01-08, 2017-01-15], (2017-01-15, 2017-01-22], (2017-01-22, 2017-01-29]], dtype='interval[datetime64[ns], right]')

pd.interval_range(start=pd.Timedelta("0 days"), periods=3, freq="9H")
# IntervalIndex([(0 days 00:00:00, 0 days 09:00:00], (0 days 09:00:00, 0 days 18:00:00], (0 days 18:00:00, 1 days 03:00:00]], dtype='interval[timedelta64[ns], right]')

さらに、closed引数を使用して、インターバルを閉じる側を指定できます。デフォルトでは、インターバルは右側で閉じられます。

pd.interval_range(start=0, end=4, closed="both")
# IntervalIndex([[0, 1], [1, 2], [2, 3], [3, 4]], dtype='interval[int64, both]')

pd.interval_range(start=0, end=4, closed="neither")
# IntervalIndex([(0, 1), (1, 2), (2, 3), (3, 4)], dtype='interval[int64, neither]')

startendperiodsを指定すると、結果として得られるIntervalIndexは、startからendまで等間隔にperiodsの数だけ要素があるインターバルが作成されます。

pd.interval_range(start=0, end=6, periods=4)
# IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]], dtype='interval[float64, right]')

pd.interval_range(pd.Timestamp("2018-01-01"), pd.Timestamp("2018-02-28"), periods=3)
# IntervalIndex([(2018-01-01, 2018-01-20 08:00:00], (2018-01-20 08:00:00, 2018-02-08 16:00:00], (2018-02-08 16:00:00, 2018-02-28]], dtype='interval[datetime64[ns], right]')

その他のインデックス作成に関するFAQ

整数による索引

整数軸ラベルを使用したラベルベースのインデックス付けは、厄介なトピックです。メーリングリストや科学的なPythonコミュニティのさまざまなメンバーの間で頻繁に議論されています。pandasでは、私たちの一般的な視点は、ラベルが整数の位置よりも重要であるということです。したがって、整数軸インデックスでは、.locなどの標準ツールではラベルベースのインデックス付けのみが可能です。次のコードは例外を生成します。

s = pd.Series(range(5))

s[-1]
# ---------------------------------------------------------------------------
# ValueError                                Traceback (most recent call last)
# File ~/work/pandas/pandas/pandas/core/indexes/range.py:385, in RangeIndex.get_loc(self, key, method, tolerance)
#     384 try:
# --> 385     return self._range.index(new_key)
#     386 except ValueError as err:
#
# ValueError: -1 is not in range
#
# The above exception was the direct cause of the following exception:
#
# KeyError                                  Traceback (most recent call last)
# Input In [217], in <module>
# ----> 1 s[-1]
#
# File ~/work/pandas/pandas/pandas/core/series.py:958, in Series.__getitem__(self, key)
#     955     return self._values[key]
#     957 elif key_is_scalar:
# --> 958     return self._get_value(key)
#     960 if is_hashable(key):
#     961     # Otherwise index.get_value will raise InvalidIndexError
#     962     try:
#     963         # For labels that don't resolve as scalars like tuples and frozensets
#
# File ~/work/pandas/pandas/pandas/core/series.py:1066, in Series._get_value(self, label, takeable)
#    1063     return self._values[label]
#    1065 # Similar to Index.get_value, but we do not fall back to positional
# -> 1066 loc = self.index.get_loc(label)
#    1067 return self.index._get_values_for_loc(self, loc, label)

# File ~/work/pandas/pandas/pandas/core/indexes/range.py:387, in RangeIndex.get_loc(self, key, method, tolerance)
#     385         return self._range.index(new_key)
#     386     except ValueError as err:
# --> 387         raise KeyError(key) from err
#     388 self._check_indexing_error(key)
#     389 raise KeyError(key)
#
# KeyError: -1

df = pd.DataFrame(np.random.randn(5, 4))

df
#           0         1         2         3
# 0 -0.130121 -0.476046  0.759104  0.213379
# 1 -0.082641  0.448008  0.656420 -1.051443
# 2  0.594956 -0.151360 -0.069303  1.221431
# 3 -0.182832  0.791235  0.042745  2.069775
# 4  1.446552  0.019814 -1.389212 -0.702312

df.loc[-2:]
#           0         1         2         3
# 0 -0.130121 -0.476046  0.759104  0.213379
# 1 -0.082641  0.448008  0.656420 -1.051443
# 2  0.594956 -0.151360 -0.069303  1.221431
# 3 -0.182832  0.791235  0.042745  2.069775
# 4  1.446552  0.019814 -1.389212 -0.702312

この意図的な決定は、あいまいさと微妙なバグを防ぐために行われました(位置ベースのインデックスを「フォールバック」しなくなるようにAPIを変更したところ、多くのユーザーがバグを見つけたと報告しました)。

非単調なインデックスには完全一致が必要

SeriesまたはDataFrameのインデックスが単調に増加または減少している場合、通常のPythonリストのスライスインデックス付けのように、ラベルベースのスライスの境界がインデックスの範囲外にある可能性があります。インデックスの単調性は、is_monotonic_increasing()およびis_monotonic_decreasing()属性でテストできます。

df = pd.DataFrame(index=[2, 3, 3, 4, 5], columns=["data"], data=list(range(5)))

df.index.is_monotonic_increasing
# True

# 行0,1は存在しないが、行2,3(両方とも),4を返す
df.loc[0:4, :]
#    data
# 2     0
# 3     1
# 3     2
# 4     3

# スライスがインデックス外にあるため、空のDataFrameが返される
df.loc[13:15, :]
# Empty DataFrame
# Columns: [data]
# Index: []

一方、インデックスが単調でない場合、両方のスライス境界はインデックスの一意の値でなければなりません。

df = pd.DataFrame(index=[2, 3, 1, 4, 3, 5], columns=["data"], data=list(range(6)))

df.index.is_monotonic_increasing
# False

# 2も4もインデックスに存在するので問題ない
df.loc[2:4, :]
#    data
# 2     0
# 3     1
# 1     2
# 4     3
# 0はインデックスに存在しない
df.loc[0:4, :]
# KeyError: 0

# 3は一意なラベルではない
df.loc[2:3, :]
# KeyError: 'Cannot get right slice bound for non-unique label: 3'

Index.is_monotonic_increasingおよびIndex.is_monotonic_decreasingは、インデックスが単調であるかどうかを軽くチェックするだけです。厳密な単調性を確認するには、そのどちらかをis_unique()属性と組み合わせます。

weakly_monotonic = pd.Index(["a", "b", "c", "c"])

weakly_monotonic
# Index(['a', 'b', 'c', 'c'], dtype='object')

weakly_monotonic.is_monotonic_increasing
# True

weakly_monotonic.is_monotonic_increasing & weakly_monotonic.is_unique
# False

終点は含まれる

標準のPythonシーケンスのスライスが終点を含まないのと違い、pandasのラベルベースのスライスは終点が含まれます。この主な理由は、インデックス内の特定のラベルの後の「後続ラベル」または次の要素を簡単に判別できないことが多いためです。たとえば、次のSeiresを考えてみてください。

s = pd.Series(np.random.randn(6), index=list("abcdef"))

s
# a    0.301379
# b    1.240445
# c   -0.846068
# d   -0.043312
# e   -1.658747
# f   -0.819549
# dtype: float64

整数を使用してcからeをスライスしたいとします。これは次のようにして実行します。

s[2:5]
# c   -0.846068
# d   -0.043312
# e   -1.658747
# dtype: float64

しかし、ceのみから指定する場合、インデックスにおける次の要素を決定するのは多少複雑になる可能性があります。たとえば、次は機能しません。

s.loc['c':'e' + 1]

非常に一般的な使用例は、2つの特定の日付で開始および終了するような特定時系列を指定することです。これを可能にするために、ラベルベースのスライスに両方のエンドポイントが含まれるような設計にしました。

s.loc["c":"e"]
# c   -0.846068
# d   -0.043312
# e   -1.658747
# dtype: float64

これは間違いなく「実用性が純粋さよりも優れている」ものですが、ラベルベースのスライスが標準のPython整数スライスとまったく同じように動作することを期待する場合は注意が必要です。

暗黙的にSeriesのdtypeが変更されるインデックス付け

異なるインデックス操作により、Seriesのdtypeが変更される可能性があります。

series1 = pd.Series([1, 2, 3])

series1.dtype
# dtype('int64')

res = series1.reindex([0, 4])

res.dtype
# dtype('float64')

res
# 0    1.0
# 4    NaN
# dtype: float64
series2 = pd.Series([True])

series2.dtype
# dtype('bool')

res = series2.reindex_like(series1)

res.dtype
# dtype('O')

res
# 0    True
# 1     NaN
# 2     NaN
# dtype: object

これは、上記の(再)インデックス操作によりNaNが暗黙的に挿入され、それに応じてdtypeが変更されるためです。これにより、numpy.logical_andなどのnumpy ufuncsを使用するときに問題が発生する可能性があります。

詳細については、GH2388を参照してください。

6
13
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
6
13