MultiIndex / расширенная индексация#

Этот раздел охватывает индексирование с MultiIndex и другие расширенные возможности индексирования.

См. Индексирование и выбор данных для общей документации по индексированию.

Предупреждение

Возвращается ли копия или ссылка для операции установки может зависеть от контекста. Это иногда называется chained assignment и следует избегать. См. Возврат представления (view) против копии (copy).

См. cookbook для некоторых продвинутых стратегий.

Иерархическое индексирование (MultiIndex)#

Иерархическая / многоуровневая индексация очень интересна, так как открывает возможности для довольно сложного анализа и обработки данных, особенно при работе с данными более высокой размерности. По сути, она позволяет хранить и обрабатывать данные с произвольным количеством измерений в структурах данных меньшей размерности, таких как Series (1d) и DataFrame (2d).

В этом разделе мы покажем, что именно мы подразумеваем под «иерархическим» индексированием и как оно интегрируется со всей функциональностью индексирования pandas, описанной выше и в предыдущих разделах. Позже, при обсуждении группировать по и сводка и преобразование данных, мы покажем нетривиальные приложения, чтобы проиллюстрировать, как это помогает структурировать данные для анализа.

См. cookbook для некоторых продвинутых стратегий.

Создание объекта MultiIndex (иерархического индекса)#

The MultiIndex объект является иерархическим аналогом стандартного Index объект, который обычно хранит метки осей в объектах pandas. Вы можете думать о 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

Когда вам нужны все пары элементов из двух итерируемых объектов, может быть проще использовать MultiIndex.from_product() method:

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 из DataFrame непосредственно, используя метод MultiIndex.from_frame(). Это дополнительный метод к 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

Мы "разрежили" верхние уровни индексов, чтобы сделать вывод в консоли немного легче для глаз. Обратите внимание, что отображение индекса можно контролировать с помощью multi_sparse опция в pandas.set_options():

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#

Одной из важных особенностей иерархического индексирования является возможность выбора данных по "частичной" метке, идентифицирующей подгруппу в данных. Частичный выбор "удаляет" уровни иерархического индекса в результате полностью аналогично выбору столбца в обычном 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

См. Сечение с иерархическим индексом для выбора на более глубоком уровне.

Определенные уровни#

The 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 на осях будет работать, как ожидается; выравнивание данных будет работать так же, как Index кортежей:

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

The reindex() метод Series/DataFrames может быть вызван с другим 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

Расширенная индексация с иерархическим индексом#

Синтаксическая интеграция MultiIndex в расширенном индексировании с .loc является довольно сложной задачей, но мы приложили все усилия для этого. В целом, ключи 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 при индексации. В то время как кортеж интерпретируется как один многоуровневый ключ, список используется для указания нескольких ключей. Или, другими словами, кортежи идут горизонтально (проходя по уровням), а списки идут вертикально (сканируя уровни).

Важно, что список кортежей индексирует несколько полных 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 спецификатор, означающий индексатор для index и для столбцы. Существуют неоднозначные случаи, когда переданный индексатор может быть неверно интерпретирован как индексация оба оси, а не, скажем, в 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]

Вы можете использовать pandas.IndexSlice для облегчения более естественного синтаксиса с использованием :, а не использование slice(None).

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]

Используя булевый индексатор, вы можете предоставить выбор, связанный с values.

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

Вы также можете указать axis аргумент для .loc для интерпретации переданных срезов на одной оси.

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]

Кроме того, вы можете set значения с использованием следующих методов.

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]

Поперечное сечение#

The xs() метод DataFrame дополнительно принимает аргумент level для выбора данных на определенном уровне 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=False to xs чтобы сохранить уровень, который был выбран.

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

Продвинутое переиндексирование и выравнивание#

Используя параметр level в reindex() и align() методы объектов pandas полезны для широковещательной передачи значений по уровню. Например:

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#

The swaplevel() метод может изменить порядок двух уровней:

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#

The reorder_levels() метод обобщает swaplevel метод, позволяющий переставить уровни иерархического индекса за один шаг:

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#

The rename() метод используется для переименования меток MultiIndex, и обычно используется для переименования столбцов DataFrame. columns аргумент rename позволяет указать словарь, который включает только столбцы, которые вы хотите переименовать.

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

The 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 являются индексом, так что использование rename_axis с columns аргумент изменит имя этого индекса.

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

Оба rename и rename_axis поддерживает указание словаря, Series или функция отображения для сопоставления меток/имен с новыми значениями.

При работе с Index объект напрямую, а не через DataFrame, 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-ed объекты для эффективного индексирования и срезания, они должны быть отсортированы. Как и с любым индексом, вы можете использовать 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
qux  one   -0.251905
     two   -2.213588
bar  two    1.063327
     one    1.266143
foo  one    0.299368
baz  one   -0.863838
foo  two    0.408204
dtype: float64

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

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

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

Вы также можете передать имя уровня в sort_index если MultiIndex уровни имеют имена.

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

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

In [110]: s.sort_index(level="L2")
Out[110]: 
L1   L2 
bar  one    1.266143
baz  one   -0.863838
foo  one    0.299368
qux  one   -0.251905
bar  two    1.063327
baz  two    0.206053
foo  two    0.408204
qux  two   -2.213588
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:1192, in _LocationIndexer.__getitem__(self, key)
   1190 maybe_callable = com.apply_if_callable(key, self.obj)
   1191 maybe_callable = self._check_deprecated_callable_usage(key, maybe_callable)
-> 1192 return self._getitem_axis(maybe_callable, axis=axis)

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

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

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6708, in Index.slice_indexer(self, start, end, step)
   6664 def slice_indexer(
   6665     self,
   6666     start: Hashable | None = None,
   6667     end: Hashable | None = None,
   6668     step: int | None = None,
   6669 ) -> slice:
   6670     """
   6671     Compute the slice indexer for input labels and step.
   6672 
   (...)
   6706     slice(1, 3, None)
   6707     """
-> 6708     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6710     # return a slice
   6711     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:6934, in Index.slice_locs(self, start, end, step)
   6932 start_slice = None
   6933 if start is not None:
-> 6934     start_slice = self.get_slice_bound(start, "left")
   6935 if start_slice is None:
   6936     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)'

The is_monotonic_increasing() метод на MultiIndex показывает, отсортирован ли индекс:

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 ndarrays, pandas Index, Series, и 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

Для DataFrames указанные индексы должны быть одномерным списком или 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

Важно отметить, что take метод на объектах pandas не предназначен для работы с булевыми индексами и может возвращать неожиданные результаты.

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)
   .....: 
249 us +- 3.23 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
74.4 us +- 1.42 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)
   .....: 
150 us +- 3.01 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
139 us +- 20.3 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)

Типы индексов#

Мы обсудили MultiIndex в предыдущих разделах довольно подробно. Документация о DatetimeIndex и PeriodIndex показаны здесь, и документация о 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

The 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 вернёт CategoricalIndex, индексированный в соответствии с категориями передан Categorical тип данных. Это позволяет произвольно индексировать их даже со значениями не в категориях, аналогично тому, как можно переиндексировать любой индекс 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#

RangeIndex является подклассом Index который предоставляет индекс по умолчанию для всех DataFrame и Series объекты. RangeIndex является оптимизированной версией Index Печать широкого DataFrame типы диапазонов. A RangeIndex всегда будет иметь int64 тип данных.

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)

A RangeIndex будет вести себя аналогично Index с int64 dtype и операции над RangeIndex, результат которого не может быть представлен RangeIndex, но должен иметь целочисленный dtype, будет преобразован в Index с int64. Например:

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

IntervalIndex#

IntervalIndex вместе с его собственным dtype, IntervalDtype а также Interval скалярный тип, позволяет обеспечить первоклассную поддержку в pandas для интервальной нотации.

The IntervalIndex позволяет некоторую уникальную индексацию и также используется как тип возвращаемого значения для категорий в cut() и qcut().

Индексирование с помощью IntervalIndex#

An IntervalIndex может использоваться в Series и в 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

Попытка выбрать Interval который не содержится точно в IntervalIndex вызовет 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:1192, in _LocationIndexer.__getitem__(self, key)
   1190 maybe_callable = com.apply_if_callable(key, self.obj)
   1191 maybe_callable = self._check_deprecated_callable_usage(key, maybe_callable)
-> 1192 return self._getitem_axis(maybe_callable, axis=axis)

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

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

File ~/work/pandas/pandas/pandas/core/generic.py:4323, in NDFrame.xs(self, key, axis, level, drop_level)
   4321             new_index = index[loc]
   4322 else:
-> 4323     loc = index.get_loc(key)
   4325     if isinstance(loc, np.ndarray):
   4326         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')

Выбрать все Intervals которые перекрывают заданный Interval может быть выполнена с помощью 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

Биннинг данных с cut и qcut#

cut() и qcut() оба возвращают Categorical объект, и создаваемые ими бины хранятся как IntervalIndex в его .categories атрибут.

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() также принимает IntervalIndex для его bins аргумент, который включает полезную идиому pandas. Сначала мы вызываем cut() с некоторыми данными и bins установите фиксированное число для генерации интервалов. Затем мы передаем значения .categories как bins аргумент в последующих вызовах cut(), предоставляя новые данные, которые будут разбиты на те же интервалы.

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() функцию для создания IntervalIndex используя различные комбинации start, end, и periods. Частота по умолчанию для interval_range равен 1 для числовых интервалов и календарному дню для интервалов, подобных дате-времени:

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

The freq параметр может использоваться для указания нестандартных частот и может использовать различные псевдонимы частоты с интервалами типа datetime:

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

Указание start, end, и periods сгенерирует диапазон равномерно распределенных интервалов от start to end включительно, с periods количество элементов в результирующем IntervalIndex:

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

Часто задаваемые вопросы по индексации (разное)#

Целочисленная индексация#

Индексация по меткам с целочисленными метками осей — сложная тема. Она активно обсуждалась в списках рассылки и среди различных членов научного сообщества Python. В pandas наша общая точка зрения заключается в том, что метки важнее целочисленных позиций. Поэтому с целочисленным индексом оси only индексирование по меткам возможно с помощью стандартных инструментов, таких как .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:1133, in Series.__getitem__(self, key)
   1130     return self._values[key]
   1132 elif key_is_scalar:
-> 1133     return self._get_value(key)
   1135 # Convert generator to list before going through hashable part
   1136 # (We will iterate through the generator there to check for slices)
   1137 if is_iterator(key):

File ~/work/pandas/pandas/pandas/core/series.py:1249, in Series._get_value(self, label, takeable)
   1246     return self._values[label]
   1248 # Similar to Index.get_value, but we do not fall back to positional
-> 1249 loc = self.index.get_loc(label)
   1251 if is_integer(loc):
   1252     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:1185, in _LocationIndexer.__getitem__(self, key)
   1183     if self._is_scalar_access(key):
   1184         return self.obj._get_value(*key, takeable=self._takeable)
-> 1185     return self._getitem_tuple(key)
   1186 else:
   1187     # we by definition only have the 0th axis
   1188     axis = self.axis or 0

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

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

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

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

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6708, in Index.slice_indexer(self, start, end, step)
   6664 def slice_indexer(
   6665     self,
   6666     start: Hashable | None = None,
   6667     end: Hashable | None = None,
   6668     step: int | None = None,
   6669 ) -> slice:
   6670     """
   6671     Compute the slice indexer for input labels and step.
   6672 
   (...)
   6706     slice(1, 3, None)
   6707     """
-> 6708     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6710     # return a slice
   6711     if not is_scalar(start_slice):

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6934, in Index.slice_locs(self, start, end, step)
   6932 start_slice = None
   6933 if start is not None:
-> 6934     start_slice = self.get_slice_bound(start, "left")
   6935 if start_slice is None:
   6936     start_slice = 0

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6859, in Index.get_slice_bound(self, label, side)
   6856         return self._searchsorted_monotonic(label, side)
   6857     except ValueError:
   6858         # raise the original KeyError
-> 6859         raise err
   6861 if isinstance(slc, np.ndarray):
   6862     # get_loc may return a boolean array, which
   6863     # is OK as long as they are representable by a slice.
   6864     assert is_bool_dtype(slc.dtype)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6853, in Index.get_slice_bound(self, label, side)
   6851 # we need to look up the label
   6852 try:
-> 6853     slc = self.get_loc(label)
   6854 except KeyError as err:
   6855     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:1185, in _LocationIndexer.__getitem__(self, key)
   1183     if self._is_scalar_access(key):
   1184         return self.obj._get_value(*key, takeable=self._takeable)
-> 1185     return self._getitem_tuple(key)
   1186 else:
   1187     # we by definition only have the 0th axis
   1188     axis = self.axis or 0

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

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

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

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

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6708, in Index.slice_indexer(self, start, end, step)
   6664 def slice_indexer(
   6665     self,
   6666     start: Hashable | None = None,
   6667     end: Hashable | None = None,
   6668     step: int | None = None,
   6669 ) -> slice:
   6670     """
   6671     Compute the slice indexer for input labels and step.
   6672 
   (...)
   6706     slice(1, 3, None)
   6707     """
-> 6708     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6710     # return a slice
   6711     if not is_scalar(start_slice):

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6940, in Index.slice_locs(self, start, end, step)
   6938 end_slice = None
   6939 if end is not None:
-> 6940     end_slice = self.get_slice_bound(end, "right")
   6941 if end_slice is None:
   6942     end_slice = len(self)

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6867, in Index.get_slice_bound(self, label, side)
   6865     slc = lib.maybe_booleans_to_slice(slc.view("u1"))
   6866     if isinstance(slc, np.ndarray):
-> 6867         raise KeyError(
   6868             f"Cannot get {side} slice bound for non-unique "
   6869             f"label: {repr(original_label)}"
   6870         )
   6872 if isinstance(slc, slice):
   6873     if side == "left":

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

Index.is_monotonic_increasing и Index.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 to e, используя целые числа, это можно было бы сделать так:

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

Однако, если у вас был только c и e, определение следующего элемента в индексе может быть несколько сложным. Например, следующее не работает:

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

Очень распространённый случай использования — ограничить временной ряд начальной и конечной датами. Для этого мы приняли дизайнерское решение, чтобы срез по меткам включал обе конечные точки:

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

Это определённо случай «практичность побеждает чистоту», но на это стоит обратить внимание, если вы ожидаете, что срез по меткам будет вести себя точно так же, как стандартный целочисленный срез в Python.

Индексирование может изменить базовый dtype Series#

Различные операции индексирования могут потенциально изменить тип данных Series.

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

Это происходит потому, что операции (пере)индексации выше молча вставляют NaNs и dtype изменяется соответствующим образом. Это может вызвать некоторые проблемы при использовании numpy ufuncs такие как numpy.logical_and.

См. GH 2388 для более подробного обсуждения.