MultiIndex / расширенная индексация
В этом разделе рассматриваются индексация с помощью MultiIndex и другие возможности расширенной индексации.
Общие сведения об индексации см. в разделе Индексация и выбор данных.
Предупреждение
Возврат копии или ссылки при выполнении операции присваивания может зависеть от контекста. Это явление иногда называют chained assignment, и его следует избегать. См. раздел Возврат представления или копии.
Некоторые продвинутые стратегии описаны в сборнике рецептов.
Иерархическая индексация (MultiIndex)
Иерархическая / многоуровневая индексация открывает возможности для довольно сложного анализа и обработки данных, особенно при работе с данными высокой размерности. По сути, она позволяет хранить и обрабатывать данные с произвольным числом измерений в структурах данных меньшей размерности, таких как Series (1D) и DataFrame (2D).
В этом разделе мы объясним, что именно подразумевается под «иерархической» индексацией и как она сочетается со всеми описанными выше и в предыдущих разделах возможностями индексации pandas. Далее, при обсуждении группировки и сводных таблиц и изменения формы данных, мы рассмотрим нетривиальные примеры, показывающие, как это помогает структурировать данные для анализа.
Некоторые продвинутые стратегии описаны в сборнике рецептов.
Создание объекта MultiIndex (иерархического индекса)
Объект 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():
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
О том, как выбирать данные на более глубоком уровне, см. раздел Срез по иерархическому индексу.
Определённые уровни
Объект 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
Метод 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 следует указывать все оси, то есть индексатор для индекса и для столбцов. В некоторых неоднозначных случаях переданный индексатор может быть истолкован как индексация обеих осей, а не, например, как индексация 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
Кроме того, можно указать аргумент 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]
Кроме того, значения можно задать следующими способами.
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]
Срез по сечению
Метод 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
Методу xs можно передать drop_level=False, чтобы сохранить выбранный уровень.
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
Метод 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
Метод 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
Метод 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
Метод 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 и выполнять срезы, их нужно отсортировать. Как и для любого индекса, можно использовать 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)'
Метод 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 ndarray, объекты 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
Для DataFrame переданные индексы должны представлять собой одномерный список или 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)
.....:
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 в предыдущих разделах. Документация по 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
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
Операции groupby с индексом также сохраняют его природу.
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, переданным в качестве dtype. Это позволяет произвольно индексировать их даже значениями, не входящими в категории, подобно тому, как можно переиндексировать любой индекс 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, которая может представлять монотонно упорядоченный набор. Они аналогичны типам range в Python. 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)
RangeIndex будет вести себя аналогично Index с типом int64, а операции с RangeIndex, результат которых нельзя представить как RangeIndex, но который должен иметь целочисленный тип, будут преобразованы в Index с int64. Например:
In [181]: idx[[0, 2]]
Out[181]: Index([0, 2], dtype='int64')
IntervalIndex
IntervalIndex вместе с собственным типом IntervalDtype, а также скалярным типом Interval, обеспечивают полноценную поддержку интервальной нотации в pandas.
IntervalIndex позволяет выполнять некоторые уникальные операции индексирования, а также используется в качестве возвращаемого типа для категорий в cut() и qcut().
Индексирование с помощью IntervalIndex
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: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')
Выбрать все 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 фиксированным числом, чтобы создать интервалы. Затем при последующих вызовах cut() передадим значения .categories в качестве аргумента 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(), чтобы создать IntervalIndex, задав различные комбинации start, end и periods. Частота по умолчанию для interval_range равна 1 для числовых интервалов и одному календарному дню для интервалов типа datetime:
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 можно использовать различные псевдонимы частот:
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 до 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]')
Другие часто задаваемые вопросы об индексировании
Индексирование целыми числами
Индексирование по меткам целочисленной оси — непростая тема. Она активно обсуждалась в списках рассылки и среди различных участников сообщества научного 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 монотонно возрастает или убывает, границы среза по меткам могут выходить за пределы диапазона индекса, как и при срезе обычного объекта list в Python. Монотонность индекса можно проверить с помощью атрибутов 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_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 до 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.
Индексирование может изменить тип underlying 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.
© 2008–2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
© 2011–2025, Open source contributors
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/2.3.0/user_guide/advanced.html