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 для более подробного обсуждения.