MultiIndex / 高度なインデックス処理#

このセクションでは、MultiIndex を使用したインデックス処理その他の高度なインデックス処理機能について説明します。

一般的なインデックス処理のドキュメントについては、データのインデックス処理と選択を参照してください。

警告

設定操作でコピーが返されるか参照が返されるかは、コンテキストによって異なります。これはchained assignmentと呼ばれることもあり、避けるべきです。ビューを返すかコピーを返すかを参照してください。

高度な戦略については、クックブックを参照してください。

階層型インデックス処理 (MultiIndex)#

階層型/多段階インデックス処理は、特に高次元データを扱う上で、かなり高度なデータ分析と操作を可能にするため、非常に興味深いものです。本質的に、Series (1次元) や DataFrame (2次元) のような低次元データ構造に、任意の数の次元を持つデータを格納し、操作できるようにします。

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

高度な戦略については、クックブックを参照してください。

MultiIndex (階層型インデックス) オブジェクトの作成#

MultiIndex オブジェクトは、通常 pandas オブジェクトの軸ラベルを格納する標準の Index オブジェクトの階層的な類推物です。MultiIndex は、各タプルが一意であるタプルの配列と考えることができます。MultiIndex は、配列のリスト (MultiIndex.from_arrays() を使用)、タプルの配列 (MultiIndex.from_tuples() を使用)、イテラブルの交差集合 (MultiIndex.from_product() を使用)、または DataFrame (MultiIndex.from_frame() を使用) から作成できます。Index コンストラクタは、タプルのリストが渡された場合、MultiIndex を返そうとします。以下の例は、MultiIndex を初期化するさまざまな方法を示しています。

In [1]: arrays = [
   ...:     ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
   ...:     ["one", "two", "one", "two", "one", "two", "one", "two"],
   ...: ]
   ...: 

In [2]: tuples = list(zip(*arrays))

In [3]: tuples
Out[3]: 
[('bar', 'one'),
 ('bar', 'two'),
 ('baz', 'one'),
 ('baz', 'two'),
 ('foo', 'one'),
 ('foo', 'two'),
 ('qux', 'one'),
 ('qux', 'two')]

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

In [5]: index
Out[5]: 
MultiIndex([('bar', 'one'),
            ('bar', 'two'),
            ('baz', 'one'),
            ('baz', 'two'),
            ('foo', 'one'),
            ('foo', 'two'),
            ('qux', 'one'),
            ('qux', 'two')],
           names=['first', 'second'])

In [6]: s = pd.Series(np.random.randn(8), index=index)

In [7]: s
Out[7]: 
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() メソッドを使用する方が簡単です。

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

In [9]: pd.MultiIndex.from_product(iterables, names=["first", "second"])
Out[9]: 
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() と補完的なメソッドです。

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

In [11]: pd.MultiIndex.from_frame(df)
Out[11]: 
MultiIndex([('bar', 'one'),
            ('bar', 'two'),
            ('foo', 'one'),
            ('foo', 'two')],
           names=['first', 'second'])

便宜上、配列のリストを Series または DataFrame に直接渡すことで、MultiIndex を自動的に構築できます。

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

In [13]: s = pd.Series(np.random.randn(8), index=arrays)

In [14]: s
Out[14]: 
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

In [15]: df = pd.DataFrame(np.random.randn(8, 4), index=arrays)

In [16]: df
Out[16]: 
                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 が割り当てられます。

In [17]: df.index.names
Out[17]: FrozenList([None, None])

このインデックスは pandas オブジェクトのどの軸にも対応でき、インデックスのレベルの数は任意です。

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

In [19]: df
Out[19]: 
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]

In [20]: pd.DataFrame(np.random.randn(6, 6), index=index[:6], columns=index[:6])
Out[20]: 
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.set_options()multi_sparse オプションを使用して制御できることに注意してください。

In [21]: with pd.option_context("display.multi_sparse", False):
   ....:     df
   ....: 

軸上の原子ラベルとしてタプルを使用することを妨げるものは何もないことを覚えておく価値があります。

In [22]: pd.Series(np.random.randn(8), index=tuples)
Out[22]: 
(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 を生成したい場合があります。

レベルラベルの再構成#

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

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

In [24]: index.get_level_values("second")
Out[24]: Index(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'], dtype='object', name='second')

MultiIndex を持つ軸上の基本的なインデックス処理#

階層型インデックス処理の重要な機能の1つは、データのサブグループを識別する「部分的な」ラベルによってデータを選択できることです。部分的な選択は、通常の DataFrame で列を選択するのとまったく同じように、結果の階層型インデックスのレベルを「削除」します。

In [25]: df["bar"]
Out[25]: 
second       one       two
A       0.895717  0.805244
B       0.410835  0.813850
C      -1.413681  1.607920

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

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

In [28]: s["qux"]
Out[28]: 
one   -1.039575
two    0.271860
dtype: float64

より深いレベルで選択する方法については、階層型インデックスによるクロスセクションを参照してください。

定義されたレベル#

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

In [29]: df.columns.levels  # original MultiIndex
Out[29]: FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

In [30]: df[["foo","qux"]].columns.levels  # sliced
Out[30]: FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

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

In [31]: df[["foo", "qux"]].columns.to_numpy()
Out[31]: 
array([('foo', 'one'), ('foo', 'two'), ('qux', 'one'), ('qux', 'two')],
      dtype=object)

# for a specific level
In [32]: df[["foo", "qux"]].columns.get_level_values(0)
Out[32]: Index(['foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

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

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

In [34]: new_mi.levels
Out[34]: FrozenList([['foo', 'qux'], ['one', 'two']])

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

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

In [35]: s + s[:-2]
Out[35]: 
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

In [36]: s + s[::2]
Out[36]: 
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、またはタプルのリストや配列で呼び出すことができます。

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

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

階層型インデックスによる高度なインデックス処理#

.loc を使用した高度なインデックス処理に MultiIndex を構文的に統合するのは少し難しいですが、私たちはそうするためにあらゆる努力をしました。一般的に、MultiIndex のキーはタプルの形式を取ります。例えば、以下のコードは期待どおりに機能します。

In [39]: df = df.T

In [40]: df
Out[40]: 
                     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

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

この例では df.loc['bar', 'two'] も機能しますが、この短縮表記は一般的にあいまいさを生じる可能性があります。

また、.loc を使用して特定の列をインデックス付けしたい場合は、次のようにタプルを使用する必要があります。

In [42]: df.loc[("bar", "two"), "A"]
Out[42]: 0.8052440253863785

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

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

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

「部分的な」スライスも非常にうまく機能します。

In [44]: df.loc["baz":"foo"]
Out[44]: 
                     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

タプルのスライスを提供することで、「範囲」の値でスライスできます。

In [45]: df.loc[("baz", "two"):("qux", "one")]
Out[45]: 
                     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

In [46]: df.loc[("baz", "two"):"foo"]
Out[46]: 
                     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

ラベルまたはタプルのリストを渡すのは、再インデックス処理と似ています。

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

インデックス処理に関して、pandas ではタプルとリストが同じように扱われないことに注意することが重要です。タプルは1つの多レベルキーとして解釈されるのに対し、リストは複数のキーを指定するために使用されます。言い換えれば、タプルは水平方向 (レベルを横断) に、リストは垂直方向 (レベルをスキャン) に進みます。

重要なことに、タプルのリストは複数の完全な MultiIndex キーをインデックス処理しますが、リストのタプルはレベル内の複数の値を参照します。

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

In [49]: s.loc[[("A", "c"), ("B", "d")]]  # list of tuples
Out[49]: 
A  c    1
B  d    5
dtype: int64

In [50]: s.loc[(["A", "B"], ["c", "d"])]  # tuple of lists
Out[50]: 
A  c    1
   d    2
B  c    4
   d    5
dtype: int64

スライサーの使用#

複数のインデクサーを提供することで、MultiIndex をスライスできます。

スライス、ラベルのリスト、ラベル、ブールインデクサーなど、ラベルでインデックス処理する場合と同様に、任意のセレクターを指定できます。ラベルによる選択を参照してください。

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

通常通り、これはラベルインデックス処理であるため、スライサーの両側が含まれます。

警告

.loc 指定子では、インデックスと列の両方の軸を指定する必要があります。渡されたインデクサーが、行の MultiIndex ではなく、両方の軸をインデックス処理していると誤解される可能性があるあいまいなケースがいくつかあります。

これを行う必要があります。

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

これを行うべきではありません。

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

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

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

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

In [55]: dfmi
Out[55]: 
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]

スライス、リスト、ラベルを使用した基本的なMultiIndexスライス。

In [56]: dfmi.loc[(slice("A1", "A3"), slice(None), ["C1", "C3"]), :]
Out[56]: 
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]

slice(None) を使用する代わりに、pandas.IndexSlice を使用して、: を使用したより自然な構文を容易にすることができます。

In [57]: idx = pd.IndexSlice

In [58]: dfmi.loc[idx[:, :, ["C1", "C3"]], idx[:, "foo"]]
Out[58]: 
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]

この方法を使用すると、複数の軸に対して同時に非常に複雑な選択を実行できます。

In [59]: dfmi.loc["A1", (slice(None), "foo")]
Out[59]: 
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]

In [60]: dfmi.loc[idx[:, :, ["C1", "C3"]], idx[:, "foo"]]
Out[60]: 
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]

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

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

In [62]: dfmi.loc[idx[mask, :, ["C1", "C3"]], idx[:, "foo"]]
Out[62]: 
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 引数を指定して、渡されたスライサーを単一の軸で解釈することもできます。

In [63]: dfmi.loc(axis=0)[:, :, ["C1", "C3"]]
Out[63]: 
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]

さらに、以下のメソッドを使用して値を設定できます。

In [64]: df2 = dfmi.copy()

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

In [66]: df2
Out[66]: 
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]

アライメント可能なオブジェクトの右辺も使用できます。

In [67]: df2 = dfmi.copy()

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

In [69]: df2
Out[69]: 
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]

クロスセクション#

DataFramexs() メソッドは、MultiIndex の特定のレベルでデータを簡単に選択できるように、レベル引数を追加で受け取ります。

In [70]: df
Out[70]: 
                     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

In [71]: df.xs("one", level="second")
Out[71]: 
              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
# using the slicers
In [72]: df.loc[(slice(None), "one"), :]
Out[72]: 
                     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

xs を使用して列を選択することもでき、axis引数を指定します。

In [73]: df = df.T

In [74]: df.xs("one", level="second", axis=1)
Out[74]: 
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
# using the slicers
In [75]: df.loc[:, (slice(None), "one")]
Out[75]: 
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 は複数のキーによる選択も可能です。

In [76]: df.xs(("one", "bar"), level=("second", "first"), axis=1)
Out[76]: 
first        bar
second       one
A       0.895717
B       0.410835
C      -1.413681
# using the slicers
In [77]: df.loc[:, ("bar", "one")]
Out[77]: 
A    0.895717
B    0.410835
C   -1.413681
Name: (bar, one), dtype: float64

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

In [78]: df.xs("one", level="second", axis=1, drop_level=False)
Out[78]: 
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 (デフォルト値) を使用した結果と比較してください。

In [79]: df.xs("one", level="second", axis=1, drop_level=True)
Out[79]: 
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 を使用することは、レベル全体に値をブロードキャストするのに役立ちます。例えば、

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

In [81]: df = pd.DataFrame(np.random.randn(4, 2), index=midx)

In [82]: df
Out[82]: 
               0         1
one  y  1.519970 -0.493662
     x  0.600178  0.274230
zero y  0.132885 -0.023688
     x  2.410179  1.450520

In [83]: df2 = df.groupby(level=0).mean()

In [84]: df2
Out[84]: 
             0         1
one   1.060074 -0.109716
zero  1.271532  0.713416

In [85]: df2.reindex(df.index, level=0)
Out[85]: 
               0         1
one  y  1.060074 -0.109716
     x  1.060074 -0.109716
zero y  1.271532  0.713416
     x  1.271532  0.713416

# aligning
In [86]: df_aligned, df2_aligned = df.align(df2, level=0)

In [87]: df_aligned
Out[87]: 
               0         1
one  y  1.519970 -0.493662
     x  0.600178  0.274230
zero y  0.132885 -0.023688
     x  2.410179  1.450520

In [88]: df2_aligned
Out[88]: 
               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つのレベルの順序を入れ替えることができます。

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

In [90]: df[:5].swaplevel(0, 1, axis=0)
Out[90]: 
               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ステップで階層型インデックスレベルを置換できるようにします。

In [91]: df[:5].reorder_levels([1, 0], axis=0)
Out[91]: 
               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 の名前の変更#

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

In [92]: df.rename(columns={0: "col0", 1: "col1"})
Out[92]: 
            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 のメインインデックスの特定のラベルの名前を変更するためにも使用できます。

In [93]: df.rename(index={"one": "two", "y": "z"})
Out[93]: 
               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 の名前を変更するために使用されます。特に、MultiIndex のレベルの名前を指定でき、これは後で reset_index() を使用して MultiIndex の値を列に移動する場合に便利です。

In [94]: df.rename_axis(index=["abc", "def"])
Out[94]: 
                 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 の列はインデックスであるため、columns 引数とともに rename_axis を使用すると、そのインデックスの名前が変更されることに注意してください。

In [95]: df.rename_axis(columns="Cols").columns
Out[95]: RangeIndex(start=0, stop=2, step=1, name='Cols')

renamerename_axis の両方で、ラベル/名前を新しい値にマッピングするための辞書、Series、またはマッピング関数を指定できます。

DataFrame を介さずに Index オブジェクトを直接操作する場合、Index.set_names() を使用して名前を変更できます。

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

In [97]: mi.names
Out[97]: FrozenList(['x', 'y'])

In [98]: mi2 = mi.rename("new name", level=0)

In [99]: mi2
Out[99]: 
MultiIndex([(1, 'a'),
            (1, 'b'),
            (2, 'a'),
            (2, 'b')],
           names=['new name', 'y'])

MultiIndex の名前をレベル経由で設定することはできません。

In [100]: mi.levels[0].name = "name via level"
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[100], line 1
----> 1 mi.levels[0].name = "name via level"

File ~/work/pandas/pandas/pandas/core/indexes/base.py:1697, in Index.name(self, value)
   1693 @name.setter
   1694 def name(self, value: Hashable) -> None:
   1695     if self._no_setting_name:
   1696         # Used in MultiIndex.levels to avoid silently ignoring name updates.
-> 1697         raise RuntimeError(
   1698             "Cannot set name on a level of a MultiIndex. Use "
   1699             "'MultiIndex.set_names' instead."
   1700         )
   1701     maybe_extract_name(value, None, type(self))
   1702     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() を使用できます。

In [101]: import random

In [102]: random.shuffle(tuples)

In [103]: s = pd.Series(np.random.randn(8), index=pd.MultiIndex.from_tuples(tuples))

In [104]: s
Out[104]: 
baz  two    0.206053
foo  two   -0.251905
bar  one   -2.213588
     two    1.063327
baz  one    1.266143
foo  one    0.299368
qux  one   -0.863838
     two    0.408204
dtype: float64

In [105]: s.sort_index()
Out[105]: 
bar  one   -2.213588
     two    1.063327
baz  one    1.266143
     two    0.206053
foo  one    0.299368
     two   -0.251905
qux  one   -0.863838
     two    0.408204
dtype: float64

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

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

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

In [108]: s.index = s.index.set_names(["L1", "L2"])

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

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

高次元オブジェクトでは、MultiIndex を持つ場合、他の軸をレベルでソートできます。

In [111]: df.T.sort_index(level=1, axis=1)
Out[111]: 
        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 が表示されます)、ビューではなくデータのコピーを返します。

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

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

In [114]: dfm
Out[114]: 
            jolie
jim joe          
0   x    0.490671
    x    0.120248
1   z    0.537020
    y    0.110968

In [115]: dfm.loc[(1, 'z')]
Out[115]: 
           jolie
jim joe         
1   z    0.53702

さらに、完全に字句順にソートされていないものをインデックス処理しようとすると、例外が発生する可能性があります。

In [116]: dfm.loc[(0, 'y'):(1, 'z')]
---------------------------------------------------------------------------
UnsortedIndexError                        Traceback (most recent call last)
Cell In[116], line 1
----> 1 dfm.loc[(0, 'y'):(1, 'z')]

File ~/work/pandas/pandas/pandas/core/indexing.py:1191, in _LocationIndexer.__getitem__(self, key)
   1189 maybe_callable = com.apply_if_callable(key, self.obj)
   1190 maybe_callable = self._check_deprecated_callable_usage(key, maybe_callable)
-> 1191 return self._getitem_axis(maybe_callable, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexing.py:1411, in _LocIndexer._getitem_axis(self, key, axis)
   1409 if isinstance(key, slice):
   1410     self._validate_key(key, axis)
-> 1411     return self._get_slice_axis(key, axis=axis)
   1412 elif com.is_bool_indexer(key):
   1413     return self._getbool_axis(key, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexing.py:1443, in _LocIndexer._get_slice_axis(self, slice_obj, axis)
   1440     return obj.copy(deep=False)
   1442 labels = obj._get_axis(axis)
-> 1443 indexer = labels.slice_indexer(slice_obj.start, slice_obj.stop, slice_obj.step)
   1445 if isinstance(indexer, slice):
   1446     return self.obj._slice(indexer, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6678, in Index.slice_indexer(self, start, end, step)
   6634 def slice_indexer(
   6635     self,
   6636     start: Hashable | None = None,
   6637     end: Hashable | None = None,
   6638     step: int | None = None,
   6639 ) -> slice:
   6640     """
   6641     Compute the slice indexer for input labels and step.
   6642 
   (...)
   6676     slice(1, 3, None)
   6677     """
-> 6678     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6680     # return a slice
   6681     if not is_scalar(start_slice):

File ~/work/pandas/pandas/pandas/core/indexes/multi.py:2923, in MultiIndex.slice_locs(self, start, end, step)
   2871 """
   2872 For an ordered MultiIndex, compute the slice locations for input
   2873 labels.
   (...)
   2919                       sequence of such.
   2920 """
   2921 # This function adds nothing to its parent implementation (the magic
   2922 # happens in get_slice_bound method), but it adds meaningful doc.
-> 2923 return super().slice_locs(start, end, step)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6904, in Index.slice_locs(self, start, end, step)
   6902 start_slice = None
   6903 if start is not None:
-> 6904     start_slice = self.get_slice_bound(start, "left")
   6905 if start_slice is None:
   6906     start_slice = 0

File ~/work/pandas/pandas/pandas/core/indexes/multi.py:2867, in MultiIndex.get_slice_bound(self, label, side)
   2865 if not isinstance(label, tuple):
   2866     label = (label,)
-> 2867 return self._partial_tup_index(label, side=side)

File ~/work/pandas/pandas/pandas/core/indexes/multi.py:2927, in MultiIndex._partial_tup_index(self, tup, side)
   2925 def _partial_tup_index(self, tup: tuple, side: Literal["left", "right"] = "left"):
   2926     if len(tup) > self._lexsort_depth:
-> 2927         raise UnsortedIndexError(
   2928             f"Key length ({len(tup)}) was greater than MultiIndex lexsort depth "
   2929             f"({self._lexsort_depth})"
   2930         )
   2932     n = len(tup)
   2933     start, end = 0, len(self)

UnsortedIndexError: 'Key length (2) was greater than MultiIndex lexsort depth (1)'

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

In [117]: dfm.index.is_monotonic_increasing
Out[117]: False
In [118]: dfm = dfm.sort_index()

In [119]: dfm
Out[119]: 
            jolie
jim joe          
0   x    0.490671
    x    0.120248
1   y    0.110968
    z    0.537020

In [120]: dfm.index.is_monotonic_increasing
Out[120]: True

そして、選択は期待どおりに機能します。

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

Take メソッド#

NumPy ndarray と同様に、pandas の IndexSeries、および DataFrame も、指定された軸上の指定されたインデックス位置の要素を取得する take() メソッドを提供します。指定されたインデックスは、整数のインデックス位置のリストまたは ndarray である必要があります。take は、オブジェクトの末尾からの相対位置として負の整数も受け入れます。

In [122]: index = pd.Index(np.random.randint(0, 1000, 10))

In [123]: index
Out[123]: Index([214, 502, 712, 567, 786, 175, 993, 133, 758, 329], dtype='int64')

In [124]: positions = [0, 9, 3]

In [125]: index[positions]
Out[125]: Index([214, 329, 567], dtype='int64')

In [126]: index.take(positions)
Out[126]: Index([214, 329, 567], dtype='int64')

In [127]: ser = pd.Series(np.random.randn(10))

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

In [129]: ser.take(positions)
Out[129]: 
0   -0.179666
9    1.824375
3    0.392149
dtype: float64

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

In [130]: frm = pd.DataFrame(np.random.randn(5, 3))

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

In [132]: frm.take([0, 2], axis=1)
Out[132]: 
          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 メソッドはブールインデックスでは機能することを意図しておらず、予期しない結果を返す可能性があることに注意することが重要です。

In [133]: arr = np.random.randn(10)

In [134]: arr.take([False, False, True, True])
Out[134]: array([-1.1935, -1.1935,  0.6775,  0.6775])

In [135]: arr[[0, 1]]
Out[135]: array([-1.1935,  0.6775])

In [136]: ser = pd.Series(np.random.randn(10))

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

In [138]: ser.iloc[[0, 1]]
Out[138]: 
0    0.233141
1   -0.223540
dtype: float64

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

In [139]: arr = np.random.randn(10000, 5)

In [140]: indexer = np.arange(10000)

In [141]: random.shuffle(indexer)

In [142]: %timeit arr[indexer]
   .....: %timeit arr.take(indexer, axis=0)
   .....: 
247 us +- 3.14 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
75.4 us +- 2.12 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
In [143]: ser = pd.Series(arr[:, 0])

In [144]: %timeit ser.iloc[indexer]
   .....: %timeit ser.take(indexer)
   .....: 
143 us +- 5.77 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
133 us +- 8.27 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)

インデックスの種類#

前のセクションでは MultiIndex についてかなり詳しく説明しました。DatetimeIndexPeriodIndex のドキュメントはこちらTimedeltaIndex のドキュメントはこちらにあります。

以下のサブセクションでは、他のインデックスの種類についていくつか説明します。

CategoricalIndex#

CategoricalIndex は、重複する要素を持つインデックス処理をサポートするのに便利なインデックスタイプです。Categorical をラップするコンテナであり、多数の重複する要素を持つインデックスの効率的なインデックス処理と格納を可能にします。

In [145]: from pandas.api.types import CategoricalDtype

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

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

In [148]: df
Out[148]: 
   A  B
0  0  a
1  1  a
2  2  b
3  3  b
4  4  c
5  5  a

In [149]: df.dtypes
Out[149]: 
A       int64
B    category
dtype: object

In [150]: df["B"].cat.categories
Out[150]: Index(['c', 'a', 'b'], dtype='object')

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

In [151]: df2 = df.set_index("B")

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

__getitem__/.iloc/.loc を使用したインデックス処理は、重複する要素を持つ Index と同様に機能します。インデクサーはカテゴリ内になければならず、そうでない場合は KeyError が発生します。

In [153]: df2.loc["a"]
Out[153]: 
   A
B   
a  0
a  1
a  5

インデックス処理後も CategoricalIndex保持されます

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

インデックスをソートすると、カテゴリの順序でソートされます (インデックスを CategoricalDtype(list('cab')) で作成したため、ソート順は cab です)。

In [155]: df2.sort_index()
Out[155]: 
   A
B   
c  4
a  0
a  1
a  5
b  2
b  3

インデックスに対するグループ化操作も、インデックスの性質を維持します。

In [156]: df2.groupby(level=0, observed=True).sum()
Out[156]: 
   A
B   
c  4
a  6
b  5

In [157]: df2.groupby(level=0, observed=True).sum().index
Out[157]: CategoricalIndex(['c', 'a', 'b'], categories=['c', 'a', 'b'], ordered=False, dtype='category', name='B')

再インデックス処理操作は、渡されたインデクサーのタイプに基づいて結果のインデックスを返します。リストを渡すと通常の Index が返されます。Categorical でインデックス処理すると、渡された Categorical dtype のカテゴリに従ってインデックス処理された CategoricalIndex が返されます。これにより、カテゴリにない値であっても、これらの要素を任意にインデックス処理できます。これは、任意の pandas インデックスを再インデックス処理できるのと同様です。

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

In [159]: df3 = df3.set_index("B")

In [160]: df3
Out[160]: 
   A
B   
a  0
b  1
c  2
In [161]: df3.reindex(["a", "e"])
Out[161]: 
     A
B     
a  0.0
e  NaN

In [162]: df3.reindex(["a", "e"]).index
Out[162]: Index(['a', 'e'], dtype='object', name='B')

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

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

警告

CategoricalIndex に対する再整形操作および比較操作では、同じカテゴリを持っていなければならず、そうでない場合は TypeError が発生します。

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

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

In [167]: df4 = df4.set_index("B")

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

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

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

In [171]: df5 = df5.set_index("B")

In [172]: df5.index
Out[172]: CategoricalIndex(['b', 'c'], categories=['b', 'c'], ordered=False, dtype='category', name='B')
In [173]: pd.concat([df4, df5])
Out[173]: 
   A
B   
b  0
a  1
b  0
c  1

RangeIndex#

RangeIndexIndex のサブクラスであり、すべての DataFrame および Series オブジェクトのデフォルトインデックスを提供します。RangeIndex は、単調順序付き集合を表すことができる Index の最適化されたバージョンです。これらは Python の range タイプに似ています。RangeIndex は常に int64 dtype を持ちます。

In [174]: idx = pd.RangeIndex(5)

In [175]: idx
Out[175]: RangeIndex(start=0, stop=5, step=1)

RangeIndex は、すべての DataFrame および Series オブジェクトのデフォルトインデックスです。

In [176]: ser = pd.Series([1, 2, 3])

In [177]: ser.index
Out[177]: RangeIndex(start=0, stop=3, step=1)

In [178]: df = pd.DataFrame([[1, 2], [3, 4]])

In [179]: df.index
Out[179]: RangeIndex(start=0, stop=2, step=1)

In [180]: df.columns
Out[180]: RangeIndex(start=0, stop=2, step=1)

RangeIndexint64 dtype を持つ Index と同様に動作し、結果が RangeIndex で表現できないが整数 dtype を持つべき RangeIndex 上の操作は int64 を持つ Index に変換されます。例えば、

In [181]: idx[[0, 2]]
Out[181]: Index([0, 2], dtype='int64')

IntervalIndex#

IntervalIndex は、独自の dtype である IntervalDtype およびスカラー型 Interval と共に、pandas における区間表記の第一級サポートを可能にします。

IntervalIndex はいくつかのユニークなインデックス処理を可能にし、また cut() および qcut() のカテゴリの戻り値の型としても使用されます。

IntervalIndex を使用したインデックス処理#

IntervalIndexSeries および DataFrame のインデックスとして使用できます。

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

In [183]: df
Out[183]: 
        A
(0, 1]  1
(1, 2]  2
(2, 3]  3
(3, 4]  4

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

In [184]: df.loc[2]
Out[184]: 
A    2
Name: (1, 2], dtype: int64

In [185]: df.loc[[2, 3]]
Out[185]: 
        A
(1, 2]  2
(2, 3]  3

区間内に含まれるラベルを選択すると、その区間も選択されます。

In [186]: df.loc[2.5]
Out[186]: 
A    3
Name: (2, 3], dtype: int64

In [187]: df.loc[[2.5, 3.5]]
Out[187]: 
        A
(2, 3]  3
(3, 4]  4

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

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

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

In [189]: df.loc[pd.Interval(0.5, 2.5)]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[189], line 1
----> 1 df.loc[pd.Interval(0.5, 2.5)]

File ~/work/pandas/pandas/pandas/core/indexing.py:1191, in _LocationIndexer.__getitem__(self, key)
   1189 maybe_callable = com.apply_if_callable(key, self.obj)
   1190 maybe_callable = self._check_deprecated_callable_usage(key, maybe_callable)
-> 1191 return self._getitem_axis(maybe_callable, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexing.py:1431, in _LocIndexer._getitem_axis(self, key, axis)
   1429 # fall thru to straight lookup
   1430 self._validate_key(key, axis)
-> 1431 return self._get_label(key, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexing.py:1381, in _LocIndexer._get_label(self, label, axis)
   1379 def _get_label(self, label, axis: AxisInt):
   1380     # GH#5567 this will fail if the label is not present in the axis.
-> 1381     return self.obj.xs(label, axis=axis)

File ~/work/pandas/pandas/pandas/core/generic.py:4320, in NDFrame.xs(self, key, axis, level, drop_level)
   4318             new_index = index[loc]
   4319 else:
-> 4320     loc = index.get_loc(key)
   4322     if isinstance(loc, np.ndarray):
   4323         if loc.dtype == np.bool_:

File ~/work/pandas/pandas/pandas/core/indexes/interval.py:679, in IntervalIndex.get_loc(self, key)
    677 matches = mask.sum()
    678 if matches == 0:
--> 679     raise KeyError(key)
    680 if matches == 1:
    681     return mask.argmax()

KeyError: Interval(0.5, 2.5, closed='right')

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

In [190]: idxr = df.index.overlaps(pd.Interval(0.5, 2.5))

In [191]: idxr
Out[191]: array([ True,  True,  True, False])

In [192]: df[idxr]
Out[192]: 
        A
(0, 1]  1
(1, 2]  2
(2, 3]  3

cutqcut を使用したデータビニング#

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

In [193]: c = pd.cut(range(4), bins=2)

In [194]: c
Out[194]: 
[(-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]]

In [195]: c.categories
Out[195]: IntervalIndex([(-0.003, 1.5], (1.5, 3.0]], dtype='interval[float64, right]')

cut()bins 引数として IntervalIndex も受け入れます。これにより、便利な pandas イディオムが可能になります。まず、いくつかのデータと固定数に設定された bins を指定して cut() を呼び出してビンを生成します。次に、.categories の値を後続の cut() 呼び出しの bins 引数として渡し、同じビンにビン詰めされる新しいデータを供給します。

In [196]: pd.cut([0, 3, 5, 1], bins=c.categories)
Out[196]: 
[(-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-like区間の場合は暦日です。

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

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

In [199]: pd.interval_range(end=pd.Timedelta("3 days"), periods=3)
Out[199]: 
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-likeな区間ではさまざまな頻度エイリアスを利用できます。

In [200]: pd.interval_range(start=0, periods=5, freq=1.5)
Out[200]: 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]')

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

In [202]: pd.interval_range(start=pd.Timedelta("0 days"), periods=3, freq="9h")
Out[202]: 
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 パラメータを使用して、区間のどちら側が閉じているかを指定できます。区間はデフォルトで右側が閉じられています。

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

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

startendperiods を指定すると、start から end まで均等に間隔が空けられた区間の範囲が生成され、結果の IntervalIndex には periods 個の要素が含まれます。

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

In [206]: pd.interval_range(pd.Timestamp("2018-01-01"), pd.Timestamp("2018-02-28"), periods=3)
Out[206]: 
IntervalIndex([(2018-01-01 00:00:00, 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 00:00:00]],
              dtype='interval[datetime64[ns], right]')

インデックス処理に関するその他のFAQ#

整数インデックス処理#

整数軸ラベルを用いたラベルベースのインデックス処理は厄介な問題です。これはメーリングリストや科学Pythonコミュニティの様々なメンバー間で活発に議論されてきました。pandasでは、一般的に整数位置よりもラベルの方が重要であるという見解です。したがって、整数軸インデックスを持つ場合、.loc のような標準ツールではラベルベースのインデックス処理のみが可能です。以下のコードは例外を生成します。

In [207]: s = pd.Series(range(5))

In [208]: s[-1]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/work/pandas/pandas/pandas/core/indexes/range.py:413, in RangeIndex.get_loc(self, key)
    412 try:
--> 413     return self._range.index(new_key)
    414 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)
Cell In[208], line 1
----> 1 s[-1]

File ~/work/pandas/pandas/pandas/core/series.py:1130, in Series.__getitem__(self, key)
   1127     return self._values[key]
   1129 elif key_is_scalar:
-> 1130     return self._get_value(key)
   1132 # Convert generator to list before going through hashable part
   1133 # (We will iterate through the generator there to check for slices)
   1134 if is_iterator(key):

File ~/work/pandas/pandas/pandas/core/series.py:1246, in Series._get_value(self, label, takeable)
   1243     return self._values[label]
   1245 # Similar to Index.get_value, but we do not fall back to positional
-> 1246 loc = self.index.get_loc(label)
   1248 if is_integer(loc):
   1249     return self._values[loc]

File ~/work/pandas/pandas/pandas/core/indexes/range.py:415, in RangeIndex.get_loc(self, key)
    413         return self._range.index(new_key)
    414     except ValueError as err:
--> 415         raise KeyError(key) from err
    416 if isinstance(key, Hashable):
    417     raise KeyError(key)

KeyError: -1

In [209]: df = pd.DataFrame(np.random.randn(5, 4))

In [210]: df
Out[210]: 
          0         1         2         3
0 -0.435772 -1.188928 -0.808286 -0.284634
1 -1.815703  1.347213 -0.243487  0.514704
2  1.162969 -0.287725 -0.179734  0.993962
3 -0.212673  0.909872 -0.733333 -0.349893
4  0.456434 -0.306735  0.553396  0.166221

In [211]: df.loc[-2:]
Out[211]: 
          0         1         2         3
0 -0.435772 -1.188928 -0.808286 -0.284634
1 -1.815703  1.347213 -0.243487  0.514704
2  1.162969 -0.287725 -0.179734  0.993962
3 -0.212673  0.909872 -0.733333 -0.349893
4  0.456434 -0.306735  0.553396  0.166221

この意図的な決定は、あいまいさや微妙なバグを防ぐために行われました (多くのユーザーが、API変更で位置ベースのインデックス処理への「フォールバック」が停止されたときにバグを発見したと報告しています)。

非単調インデックスは厳密な一致を要求する#

Series または DataFrame のインデックスが単調増加または単調減少の場合、通常の Python list のスライスインデックス処理と同様に、ラベルベースのスライスの境界はインデックスの範囲外になることがあります。インデックスの単調性は、is_monotonic_increasing() および is_monotonic_decreasing() 属性でテストできます。

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

In [213]: df.index.is_monotonic_increasing
Out[213]: True

# no rows 0 or 1, but still returns rows 2, 3 (both of them), and 4:
In [214]: df.loc[0:4, :]
Out[214]: 
   data
2     0
3     1
3     2
4     3

# slice is are outside the index, so empty DataFrame is returned
In [215]: df.loc[13:15, :]
Out[215]: 
Empty DataFrame
Columns: [data]
Index: []

一方、インデックスが単調でない場合、スライスの両方の境界はインデックスの一意なメンバーである必要があります。

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

In [217]: df.index.is_monotonic_increasing
Out[217]: False

# OK because 2 and 4 are in the index
In [218]: df.loc[2:4, :]
Out[218]: 
   data
2     0
3     1
1     2
4     3
 # 0 is not in the index
In [219]: df.loc[0:4, :]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/work/pandas/pandas/pandas/core/indexes/base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File ~/work/pandas/pandas/pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File ~/work/pandas/pandas/pandas/_libs/index.pyx:191, in pandas._libs.index.IndexEngine.get_loc()

File ~/work/pandas/pandas/pandas/_libs/index.pyx:234, in pandas._libs.index.IndexEngine._get_loc_duplicates()

File ~/work/pandas/pandas/pandas/_libs/index.pyx:242, in pandas._libs.index.IndexEngine._maybe_get_bool_indexer()

File ~/work/pandas/pandas/pandas/_libs/index.pyx:134, in pandas._libs.index._unpack_bool_indexer()

KeyError: 0

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[219], line 1
----> 1 df.loc[0:4, :]

File ~/work/pandas/pandas/pandas/core/indexing.py:1184, in _LocationIndexer.__getitem__(self, key)
   1182     if self._is_scalar_access(key):
   1183         return self.obj._get_value(*key, takeable=self._takeable)
-> 1184     return self._getitem_tuple(key)
   1185 else:
   1186     # we by definition only have the 0th axis
   1187     axis = self.axis or 0

File ~/work/pandas/pandas/pandas/core/indexing.py:1377, in _LocIndexer._getitem_tuple(self, tup)
   1374 if self._multi_take_opportunity(tup):
   1375     return self._multi_take(tup)
-> 1377 return self._getitem_tuple_same_dim(tup)

File ~/work/pandas/pandas/pandas/core/indexing.py:1020, in _LocationIndexer._getitem_tuple_same_dim(self, tup)
   1017 if com.is_null_slice(key):
   1018     continue
-> 1020 retval = getattr(retval, self.name)._getitem_axis(key, axis=i)
   1021 # We should never have retval.ndim < self.ndim, as that should
   1022 #  be handled by the _getitem_lowerdim call above.
   1023 assert retval.ndim == self.ndim

File ~/work/pandas/pandas/pandas/core/indexing.py:1411, in _LocIndexer._getitem_axis(self, key, axis)
   1409 if isinstance(key, slice):
   1410     self._validate_key(key, axis)
-> 1411     return self._get_slice_axis(key, axis=axis)
   1412 elif com.is_bool_indexer(key):
   1413     return self._getbool_axis(key, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexing.py:1443, in _LocIndexer._get_slice_axis(self, slice_obj, axis)
   1440     return obj.copy(deep=False)
   1442 labels = obj._get_axis(axis)
-> 1443 indexer = labels.slice_indexer(slice_obj.start, slice_obj.stop, slice_obj.step)
   1445 if isinstance(indexer, slice):
   1446     return self.obj._slice(indexer, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6678, in Index.slice_indexer(self, start, end, step)
   6634 def slice_indexer(
   6635     self,
   6636     start: Hashable | None = None,
   6637     end: Hashable | None = None,
   6638     step: int | None = None,
   6639 ) -> slice:
   6640     """
   6641     Compute the slice indexer for input labels and step.
   6642 
   (...)
   6676     slice(1, 3, None)
   6677     """
-> 6678     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6680     # return a slice
   6681     if not is_scalar(start_slice):

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6904, in Index.slice_locs(self, start, end, step)
   6902 start_slice = None
   6903 if start is not None:
-> 6904     start_slice = self.get_slice_bound(start, "left")
   6905 if start_slice is None:
   6906     start_slice = 0

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6829, in Index.get_slice_bound(self, label, side)
   6826         return self._searchsorted_monotonic(label, side)
   6827     except ValueError:
   6828         # raise the original KeyError
-> 6829         raise err
   6831 if isinstance(slc, np.ndarray):
   6832     # get_loc may return a boolean array, which
   6833     # is OK as long as they are representable by a slice.
   6834     assert is_bool_dtype(slc.dtype)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6823, in Index.get_slice_bound(self, label, side)
   6821 # we need to look up the label
   6822 try:
-> 6823     slc = self.get_loc(label)
   6824 except KeyError as err:
   6825     try:

File ~/work/pandas/pandas/pandas/core/indexes/base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 0

 # 3 is not a unique label
In [220]: df.loc[2:3, :]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[220], line 1
----> 1 df.loc[2:3, :]

File ~/work/pandas/pandas/pandas/core/indexing.py:1184, in _LocationIndexer.__getitem__(self, key)
   1182     if self._is_scalar_access(key):
   1183         return self.obj._get_value(*key, takeable=self._takeable)
-> 1184     return self._getitem_tuple(key)
   1185 else:
   1186     # we by definition only have the 0th axis
   1187     axis = self.axis or 0

File ~/work/pandas/pandas/pandas/core/indexing.py:1377, in _LocIndexer._getitem_tuple(self, tup)
   1374 if self._multi_take_opportunity(tup):
   1375     return self._multi_take(tup)
-> 1377 return self._getitem_tuple_same_dim(tup)

File ~/work/pandas/pandas/pandas/core/indexing.py:1020, in _LocationIndexer._getitem_tuple_same_dim(self, tup)
   1017 if com.is_null_slice(key):
   1018     continue
-> 1020 retval = getattr(retval, self.name)._getitem_axis(key, axis=i)
   1021 # We should never have retval.ndim < self.ndim, as that should
   1022 #  be handled by the _getitem_lowerdim call above.
   1023 assert retval.ndim == self.ndim

File ~/work/pandas/pandas/pandas/core/indexing.py:1411, in _LocIndexer._getitem_axis(self, key, axis)
   1409 if isinstance(key, slice):
   1410     self._validate_key(key, axis)
-> 1411     return self._get_slice_axis(key, axis=axis)
   1412 elif com.is_bool_indexer(key):
   1413     return self._getbool_axis(key, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexing.py:1443, in _LocIndexer._get_slice_axis(self, slice_obj, axis)
   1440     return obj.copy(deep=False)
   1442 labels = obj._get_axis(axis)
-> 1443 indexer = labels.slice_indexer(slice_obj.start, slice_obj.stop, slice_obj.step)
   1445 if isinstance(indexer, slice):
   1446     return self.obj._slice(indexer, axis=axis)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6678, in Index.slice_indexer(self, start, end, step)
   6634 def slice_indexer(
   6635     self,
   6636     start: Hashable | None = None,
   6637     end: Hashable | None = None,
   6638     step: int | None = None,
   6639 ) -> slice:
   6640     """
   6641     Compute the slice indexer for input labels and step.
   6642 
   (...)
   6676     slice(1, 3, None)
   6677     """
-> 6678     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6680     # return a slice
   6681     if not is_scalar(start_slice):

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6910, in Index.slice_locs(self, start, end, step)
   6908 end_slice = None
   6909 if end is not None:
-> 6910     end_slice = self.get_slice_bound(end, "right")
   6911 if end_slice is None:
   6912     end_slice = len(self)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6837, in Index.get_slice_bound(self, label, side)
   6835     slc = lib.maybe_booleans_to_slice(slc.view("u1"))
   6836     if isinstance(slc, np.ndarray):
-> 6837         raise KeyError(
   6838             f"Cannot get {side} slice bound for non-unique "
   6839             f"label: {repr(original_label)}"
   6840         )
   6842 if isinstance(slc, slice):
   6843     if side == "left":

KeyError: 'Cannot get right slice bound for non-unique label: 3'

Index.is_monotonic_increasingIndex.is_monotonic_decreasing は、インデックスが弱単調であることのみをチェックします。厳密な単調性をチェックするには、これらいずれかと is_unique() 属性を組み合わせることができます。

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

In [222]: weakly_monotonic
Out[222]: Index(['a', 'b', 'c', 'c'], dtype='object')

In [223]: weakly_monotonic.is_monotonic_increasing
Out[223]: True

In [224]: weakly_monotonic.is_monotonic_increasing & weakly_monotonic.is_unique
Out[224]: False

終点は含まれる#

スライスの終点が含まれない標準的なPythonシーケンススライスと比較して、pandasのラベルベースのスライスは終点が含まれます。この主な理由は、インデックス内の特定のラベルの「後続」または次の要素を簡単に決定できないことが多いからです。例えば、以下の Series を考えてみましょう。

In [225]: s = pd.Series(np.random.randn(6), index=list("abcdef"))

In [226]: s
Out[226]: 
a   -0.101684
b   -0.734907
c   -0.130121
d   -0.476046
e    0.759104
f    0.213379
dtype: float64

c から e までスライスしたいと仮定すると、整数を使用すると次のように実現できます。

In [227]: s[2:5]
Out[227]: 
c   -0.130121
d   -0.476046
e    0.759104
dtype: float64

ただし、ce しかなかった場合、インデックス内の次の要素を決定することは多少複雑になる可能性があります。たとえば、次のコードは機能しません。

In [228]: s.loc['c':'e' + 1]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[228], line 1
----> 1 s.loc['c':'e' + 1]

TypeError: can only concatenate str (not "int") to str

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

In [229]: s.loc["c":"e"]
Out[229]: 
c   -0.130121
d   -0.476046
e    0.759104
dtype: float64

これは間違いなく「実用性が純粋さに勝る」という類のものであり、ラベルベースのスライスが標準的なPythonの整数スライスと同じように動作することを期待する場合は注意すべき点です。

インデックス処理が基盤となる Series の dtype を変更する可能性#

異なるインデックス処理操作は、Series の dtype を変更する可能性があります。

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

In [231]: series1.dtype
Out[231]: dtype('int64')

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

In [233]: res.dtype
Out[233]: dtype('float64')

In [234]: res
Out[234]: 
0    1.0
4    NaN
dtype: float64
In [235]: series2 = pd.Series([True])

In [236]: series2.dtype
Out[236]: dtype('bool')

In [237]: res = series2.reindex_like(series1)

In [238]: res.dtype
Out[238]: dtype('O')

In [239]: res
Out[239]: 
0    True
1     NaN
2     NaN
dtype: object

これは、上記の (再) インデックス処理操作がサイレントに NaN を挿入し、それに応じて dtype が変更されるためです。これは numpy.logical_and のような numpyufunc を使用する際に問題を引き起こす可能性があります。

詳細な議論については、GH 2388 を参照してください。