Многоуровневый индекс / расширенный индексирование
В этом разделе рассматриваются индексирование с многоуровневым индексом и другие расширенные функции индексирования.
См. Индексирование и выбор данных для общей документации по индексированию.
Предупреждение
Возвращаемая копия или ссылка при операции установки может зависеть от контекста. Это иногда называется 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 при передаче списка кортежей. Следующие примеры демонстрируют различные способы инициализации MultiIndexes.
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 на осях, будут работать как ожидается; выравнивание данных будет работать так же, как и для индекса кортежей:
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]
Вы можете использовать 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]
Используя булевый индексатор, вы можете обеспечить выбор, связанный со значениями.
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 дополнительно принимает аргумент уровня для упрощения выбора данных на определенном уровне 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, предоставив аргумент оси.
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 в 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
Метод 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:1747, in Index.name(self, value)
1743 @name.setter
1744 def name(self, value: Hashable) -> None:
1745 if self._no_setting_name:
1746 # Used in MultiIndex.levels to avoid silently ignoring name updates.
-> 1747 raise RuntimeError(
1748 "Cannot set name on a level of a MultiIndex. Use "
1749 "'MultiIndex.set_names' instead."
1750 )
1751 maybe_extract_name(value, None, type(self))
1752 self._name = value
RuntimeError: Cannot set name on a level of a MultiIndex. Use 'MultiIndex.set_names' instead.
Используйте Index.set_names() вместо этого.
Сортировка многоуровневого индекса
Для эффективной индексации и срезов объектов с 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]:
foo two 0.206053
baz two -0.251905
foo one -2.213588
qux two 1.063327
bar two 1.266143
one 0.299368
qux one -0.863838
baz one 0.408204
dtype: float64
In [105]: s.sort_index()
Out[105]:
bar one 0.299368
two 1.266143
baz one 0.408204
two -0.251905
foo one -2.213588
two 0.206053
qux one -0.863838
two 1.063327
dtype: float64
In [106]: s.sort_index(level=0)
Out[106]:
bar one 0.299368
two 1.266143
baz one 0.408204
two -0.251905
foo one -2.213588
two 0.206053
qux one -0.863838
two 1.063327
dtype: float64
In [107]: s.sort_index(level=1)
Out[107]:
bar one 0.299368
baz one 0.408204
foo one -2.213588
qux one -0.863838
bar two 1.266143
baz two -0.251905
foo two 0.206053
qux two 1.063327
dtype: float64
Вы также можете передать имя уровня в sort_index , если уровни MultiIndex имеют имена.
In [108]: s.index.set_names(["L1", "L2"], inplace=True)
In [109]: s.sort_index(level="L1")
Out[109]:
L1 L2
bar one 0.299368
two 1.266143
baz one 0.408204
two -0.251905
foo one -2.213588
two 0.206053
qux one -0.863838
two 1.063327
dtype: float64
In [110]: s.sort_index(level="L2")
Out[110]:
L1 L2
bar one 0.299368
baz one 0.408204
foo one -2.213588
qux one -0.863838
bar two 1.266143
baz two -0.251905
foo two 0.206053
qux two 1.063327
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 [4]: dfm.loc[(1, 'z')]
PerformanceWarning: indexing past lexsort depth may impact performance.
Out[4]:
jolie
jim joe
1 z 0.64094
Кроме того, если вы попытаетесь индексировать что-то, что не полностью отсортировано лексикографически, это может вызвать:
In [5]: dfm.loc[(0, 'y'):(1, 'z')]
UnsortedIndexError: 'Key length (2) was greater than MultiIndex lexsort depth (1)'
Метод is_monotonic_increasing() на MultiIndex показывает, отсортирован ли индекс:
In [115]: dfm.index.is_monotonic_increasing
Out[115]: False
In [116]: dfm = dfm.sort_index()
In [117]: dfm
Out[117]:
jolie
jim joe
0 x 0.490671
x 0.120248
1 y 0.110968
z 0.537020
In [118]: dfm.index.is_monotonic_increasing
Out[118]: True
И теперь выборка работает как ожидается.
In [119]: dfm.loc[(0, "y"):(1, "z")]
Out[119]:
jolie
jim joe
1 y 0.110968
z 0.537020
Методы take
Аналогично массивам NumPy, pandas Index, Series, и DataFrame также предоставляют метод take(), который извлекает элементы по заданной оси в заданных индексах. Указанные индексы должны быть либо списком, либо массивом ndarray целочисленных позиций индексов. take также примет отрицательные целые числа в качестве относительных позиций к концу объекта.
In [120]: index = pd.Index(np.random.randint(0, 1000, 10))
In [121]: index
Out[121]: Int64Index([214, 502, 712, 567, 786, 175, 993, 133, 758, 329], dtype='int64')
In [122]: positions = [0, 9, 3]
In [123]: index[positions]
Out[123]: Int64Index([214, 329, 567], dtype='int64')
In [124]: index.take(positions)
Out[124]: Int64Index([214, 329, 567], dtype='int64')
In [125]: ser = pd.Series(np.random.randn(10))
In [126]: ser.iloc[positions]
Out[126]:
0 -0.179666
9 1.824375
3 0.392149
dtype: float64
In [127]: ser.take(positions)
Out[127]:
0 -0.179666
9 1.824375
3 0.392149
dtype: float64
Для DataFrames указанные индексы должны быть 1d списком или массивом ndarray, определяющим позиции строк или столбцов.
In [128]: frm = pd.DataFrame(np.random.randn(5, 3))
In [129]: frm.take([1, 4, 3])
Out[129]:
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 [130]: frm.take([0, 2], axis=1)
Out[130]:
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 [131]: arr = np.random.randn(10)
In [132]: arr.take([False, False, True, True])
Out[132]: array([-1.1935, -1.1935, 0.6775, 0.6775])
In [133]: arr[[0, 1]]
Out[133]: array([-1.1935, 0.6775])
In [134]: ser = pd.Series(np.random.randn(10))
In [135]: ser.take([False, False, True, True])
Out[135]:
0 0.233141
0 0.233141
1 -0.223540
1 -0.223540
dtype: float64
In [136]: ser.iloc[[0, 1]]
Out[136]:
0 0.233141
1 -0.223540
dtype: float64
Наконец, небольшое примечание о производительности: поскольку метод take обрабатывает более узкий диапазон входных данных, он может предложить производительность, которая значительно быстрее, чем изысканная индексация.
In [137]: arr = np.random.randn(10000, 5)
In [138]: indexer = np.arange(10000)
In [139]: random.shuffle(indexer)
In [140]: %timeit arr[indexer]
.....: %timeit arr.take(indexer, axis=0)
.....:
241 us +- 4.69 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
97.5 us +- 1.49 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
In [141]: ser = pd.Series(arr[:, 0])
In [142]: %timeit ser.iloc[indexer]
.....: %timeit ser.take(indexer)
.....:
110 us +- 2.05 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
95.3 us +- 2.68 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
Типы индексов
Мы довольно подробно обсуждали MultiIndex в предыдущих разделах. Документация по DatetimeIndex и PeriodIndex представлена здесь, а документация по TimedeltaIndex находится здесь.
В следующих подразделах мы рассмотрим некоторые другие типы индексов.
CategoricalIndex
CategoricalIndex — это тип индекса, который полезен для поддержки индексации с дубликатами. Это контейнер вокруг Categorical и позволяет эффективно индексировать и хранить индекс с большим количеством дублируемых элементов.
In [143]: from pandas.api.types import CategoricalDtype
In [144]: df = pd.DataFrame({"A": np.arange(6), "B": list("aabbca")})
In [145]: df["B"] = df["B"].astype(CategoricalDtype(list("cab")))
In [146]: df
Out[146]:
A B
0 0 a
1 1 a
2 2 b
3 3 b
4 4 c
5 5 a
In [147]: df.dtypes
Out[147]:
A int64
B category
dtype: object
In [148]: df["B"].cat.categories
Out[148]: Index(['c', 'a', 'b'], dtype='object')
Установка индекса создаст CategoricalIndex.
In [149]: df2 = df.set_index("B")
In [150]: df2.index
Out[150]: CategoricalIndex(['a', 'a', 'b', 'b', 'c', 'a'], categories=['c', 'a', 'b'], ordered=False, dtype='category', name='B')
Индексация с __getitem__/.iloc/.loc работает аналогично Index с дубликатами. Индексаторы должны находиться в категории, иначе произойдёт KeyError.
In [151]: df2.loc["a"]
Out[151]:
A
B
a 0
a 1
a 5
CategoricalIndex сохраняется после индексации:
In [152]: df2.loc["a"].index
Out[152]: CategoricalIndex(['a', 'a', 'a'], categories=['c', 'a', 'b'], ordered=False, dtype='category', name='B')
Сортировка индекса будет выполняться по порядку категорий (напоминаем, что мы создали индекс с CategoricalDtype(list('cab')), поэтому отсортированный порядок — cab).
In [153]: df2.sort_index()
Out[153]:
A
B
c 4
a 0
a 1
a 5
b 2
b 3
Операции groupby с индексом также сохранят природу индекса.
In [154]: df2.groupby(level=0).sum()
Out[154]:
A
B
c 4
a 6
b 5
In [155]: df2.groupby(level=0).sum().index
Out[155]: CategoricalIndex(['c', 'a', 'b'], categories=['c', 'a', 'b'], ordered=False, dtype='category', name='B')
Операции переиндексации вернут результирующий индекс на основе типа переданного индексатора. Передача списка вернёт обычный Index; индексация с Categorical вернёт CategoricalIndex, индексированный в соответствии с категориями переданного Categorical типа. Это позволяет произвольно индексировать их даже с значениями, не присутствующими в категориях, подобно тому, как можно переиндексировать любой индекс pandas.
In [156]: df3 = pd.DataFrame(
.....: {"A": np.arange(3), "B": pd.Series(list("abc")).astype("category")}
.....: )
.....:
In [157]: df3 = df3.set_index("B")
In [158]: df3
Out[158]:
A
B
a 0
b 1
c 2
In [159]: df3.reindex(["a", "e"])
Out[159]:
A
B
a 0.0
e NaN
In [160]: df3.reindex(["a", "e"]).index
Out[160]: Index(['a', 'e'], dtype='object', name='B')
In [161]: df3.reindex(pd.Categorical(["a", "e"], categories=list("abe")))
Out[161]:
A
B
a 0.0
e NaN
In [162]: df3.reindex(pd.Categorical(["a", "e"], categories=list("abe"))).index
Out[162]: CategoricalIndex(['a', 'e'], categories=['a', 'b', 'e'], ordered=False, dtype='category', name='B')
Предупреждение
Операции преобразования формы и сравнения с CategoricalIndex должны иметь одинаковые категории, иначе произойдёт TypeError.
In [163]: df4 = pd.DataFrame({"A": np.arange(2), "B": list("ba")})
In [164]: df4["B"] = df4["B"].astype(CategoricalDtype(list("ab")))
In [165]: df4 = df4.set_index("B")
In [166]: df4.index
Out[166]: CategoricalIndex(['b', 'a'], categories=['a', 'b'], ordered=False, dtype='category', name='B')
In [167]: df5 = pd.DataFrame({"A": np.arange(2), "B": list("bc")})
In [168]: df5["B"] = df5["B"].astype(CategoricalDtype(list("bc")))
In [169]: df5 = df5.set_index("B")
In [170]: df5.index
Out[170]: CategoricalIndex(['b', 'c'], categories=['b', 'c'], ordered=False, dtype='category', name='B')
In [1]: pd.concat([df4, df5])
TypeError: categories must match existing categories when appending
Int64Index и RangeIndex
Устаревшее с версии 1.4.0: В pandas 2.0 Index станет типом индекса по умолчанию для числовых типов вместо Int64Index, Float64Index и UInt64Index. Эти типы индексов устаревают и будут удалены в будущей версии. RangeIndex удаляться не будет, так как представляет собой оптимизированную версию целочисленного индекса.
Int64Index — это базовый фундаментальный индекс в pandas. Это неизменяемый массив, реализующий упорядоченное, нарезкой-доступное множество.
RangeIndex — это подкласс Int64Index, который обеспечивает индекс по умолчанию для всех NDFrame объектов. RangeIndex — это оптимизированная версия Int64Index для представления монотонного упорядоченного набора. Они аналогичны типам Python range.
Float64Index
Устаревшее с версии 1.4.0: Index станет типом индекса по умолчанию для числовых типов в будущем вместо Int64Index, Float64Index и UInt64Index. Эти типы индексов устаревают и будут удалены в будущей версии Pandas. RangeIndex удаляться не будет, так как представляет собой оптимизированную версию целочисленного индекса.
По умолчанию Float64Index будет автоматически создан при передаче чисел с плавающей запятой или смешанных целочисленных и чисел с плавающей запятой при создании индекса. Это позволяет использовать чисто лексический способ срезов, что делает [],ix,loc для скалярной индексации и срезов работать точно так же.
In [171]: indexf = pd.Index([1.5, 2, 3, 4.5, 5])
In [172]: indexf
Out[172]: Float64Index([1.5, 2.0, 3.0, 4.5, 5.0], dtype='float64')
In [173]: sf = pd.Series(range(5), index=indexf)
In [174]: sf
Out[174]:
1.5 0
2.0 1
3.0 2
4.5 3
5.0 4
dtype: int64
Скалярный выбор для [],.loc всегда будет лексическим. Целое число будет соответствовать равному индексу с плавающей запятой (например, 3 эквивалентно 3.0).
In [175]: sf[3]
Out[175]: 2
In [176]: sf[3.0]
Out[176]: 2
In [177]: sf.loc[3]
Out[177]: 2
In [178]: sf.loc[3.0]
Out[178]: 2
Единственная позиционная индексация — через iloc.
In [179]: sf.iloc[3]
Out[179]: 3
Скалярный индекс, которого нет, вызовет KeyError. Срезы в основном основаны на значениях индекса при использовании [],ix,loc, и всегда позиционные при использовании iloc. Исключением является случай, когда срез булев, в этом случае он всегда будет позиционным.
In [180]: sf[2:4]
Out[180]:
2.0 1
3.0 2
dtype: int64
In [181]: sf.loc[2:4]
Out[181]:
2.0 1
3.0 2
dtype: int64
In [182]: sf.iloc[2:4]
Out[182]:
3.0 2
4.5 3
dtype: int64
В индексах с плавающей запятой разрешено использование срезов с числами с плавающей запятой.
In [183]: sf[2.1:4.6]
Out[183]:
3.0 2
4.5 3
dtype: int64
In [184]: sf.loc[2.1:4.6]
Out[184]:
3.0 2
4.5 3
dtype: int64
В индексах без плавающей запятой использование срезов с числами с плавающей запятой вызовет TypeError.
In [1]: pd.Series(range(5))[3.5]
TypeError: the label [3.5] is not a proper indexer for this index type (Int64Index)
In [1]: pd.Series(range(5))[3.5:4.5]
TypeError: the slice start [3.5] is not a proper indexer for this index type (Int64Index)
Вот типичный пример использования этого типа индексации. Представьте, что у вас есть несколько нерегулярная схема индексации, похожая на timedelta, но данные записываются как числа с плавающей запятой. Например, это могут быть смещения в миллисекундах.
In [185]: dfir = pd.concat(
.....: [
.....: pd.DataFrame(
.....: np.random.randn(5, 2), index=np.arange(5) * 250.0, columns=list("AB")
.....: ),
.....: pd.DataFrame(
.....: np.random.randn(6, 2),
.....: index=np.arange(4, 10) * 250.1,
.....: columns=list("AB"),
.....: ),
.....: ]
.....: )
.....:
In [186]: dfir
Out[186]:
A B
0.0 -0.435772 -1.188928
250.0 -0.808286 -0.284634
500.0 -1.815703 1.347213
750.0 -0.243487 0.514704
1000.0 1.162969 -0.287725
1000.4 -0.179734 0.993962
1250.5 -0.212673 0.909872
1500.6 -0.733333 -0.349893
1750.7 0.456434 -0.306735
2000.8 0.553396 0.166221
2250.9 -0.101684 -0.734907
Операции выбора всегда будут работать по значениям для всех операторов выбора.
In [187]: dfir[0:1000.4]
Out[187]:
A B
0.0 -0.435772 -1.188928
250.0 -0.808286 -0.284634
500.0 -1.815703 1.347213
750.0 -0.243487 0.514704
1000.0 1.162969 -0.287725
1000.4 -0.179734 0.993962
In [188]: dfir.loc[0:1001, "A"]
Out[188]:
0.0 -0.435772
250.0 -0.808286
500.0 -1.815703
750.0 -0.243487
1000.0 1.162969
1000.4 -0.179734
Name: A, dtype: float64
In [189]: dfir.loc[1000.4]
Out[189]:
A -0.179734
B 0.993962
Name: 1000.4, dtype: float64
Можно получить первые 1 секунду (1000 мс) данных следующим образом:
In [190]: dfir[0:1000]
Out[190]:
A B
0.0 -0.435772 -1.188928
250.0 -0.808286 -0.284634
500.0 -1.815703 1.347213
750.0 -0.243487 0.514704
1000.0 1.162969 -0.287725
Если вам нужна целочисленная выборка, используйте iloc:
In [191]: dfir.iloc[0:5]
Out[191]:
A B
0.0 -0.435772 -1.188928
250.0 -0.808286 -0.284634
500.0 -1.815703 1.347213
750.0 -0.243487 0.514704
1000.0 1.162969 -0.287725
IntervalIndex
IntervalIndex вместе со своим типом данных, IntervalDtype, а также скалярным типом Interval, обеспечивают полноценную поддержку интервальной записи в pandas.
IntervalIndex позволяет использовать уникальный способ индексирования и также используется как тип возвращаемого значения для категорий в cut() и qcut().
Индексирование с IntervalIndex
IntervalIndex может использоваться в Series и в DataFrame в качестве индекса.
In [192]: df = pd.DataFrame(
.....: {"A": [1, 2, 3, 4]}, index=pd.IntervalIndex.from_breaks([0, 1, 2, 3, 4])
.....: )
.....:
In [193]: df
Out[193]:
A
(0, 1] 1
(1, 2] 2
(2, 3] 3
(3, 4] 4
Индексирование по меткам с помощью .loc по границам интервала работает так, как ожидается, выбирая конкретный интервал.
In [194]: df.loc[2]
Out[194]:
A 2
Name: (1, 2], dtype: int64
In [195]: df.loc[[2, 3]]
Out[195]:
A
(1, 2] 2
(2, 3] 3
Если вы выбираете метку, входящую в интервал, это также выберет интервал.
In [196]: df.loc[2.5]
Out[196]:
A 3
Name: (2, 3], dtype: int64
In [197]: df.loc[[2.5, 3.5]]
Out[197]:
A
(2, 3] 3
(3, 4] 4
Выбор с использованием Interval вернет только точные совпадения (начиная с pandas 0.25.0).
In [198]: df.loc[pd.Interval(1, 2)]
Out[198]:
A 2
Name: (1, 2], dtype: int64
Попытка выбрать Interval, который не полностью содержится в IntervalIndex, вызовет KeyError.
In [7]: df.loc[pd.Interval(0.5, 2.5)]
---------------------------------------------------------------------------
KeyError: Interval(0.5, 2.5, closed='right')
Выбор всех Intervals, которые перекрывают заданный Interval, можно выполнить с помощью метода overlaps() для создания булевого индексатора.
In [199]: idxr = df.index.overlaps(pd.Interval(0.5, 2.5))
In [200]: idxr
Out[200]: array([ True, True, True, False])
In [201]: df[idxr]
Out[201]:
A
(0, 1] 1
(1, 2] 2
(2, 3] 3
Разбиение данных с помощью cut и qcut
cut() и qcut() оба возвращают объект Categorical, и созданные ими интервалы хранятся как IntervalIndex в его атрибуте .categories.
In [202]: c = pd.cut(range(4), bins=2)
In [203]: c
Out[203]:
[(-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 [204]: c.categories
Out[204]: IntervalIndex([(-0.003, 1.5], (1.5, 3.0]], dtype='interval[float64, right]')
cut() также принимает IntervalIndex для аргумента bins, что позволяет использовать полезный прием pandas. Сначала вызываем cut() с некоторыми данными и bins , установленным на фиксированное число, чтобы сгенерировать интервалы. Затем мы передаем значения .categories как аргумент bins в последующие вызовы cut(), предоставляя новые данные, которые будут разбиты на те же интервалы.
In [205]: pd.cut([0, 3, 5, 1], bins=c.categories)
Out[205]:
[(-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 [206]: pd.interval_range(start=0, end=5)
Out[206]: IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]], dtype='interval[int64, right]')
In [207]: pd.interval_range(start=pd.Timestamp("2017-01-01"), periods=4)
Out[207]: IntervalIndex([(2017-01-01, 2017-01-02], (2017-01-02, 2017-01-03], (2017-01-03, 2017-01-04], (2017-01-04, 2017-01-05]], dtype='interval[datetime64[ns], right]')
In [208]: pd.interval_range(end=pd.Timedelta("3 days"), periods=3)
Out[208]: 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 может использоваться для указания частот, отличных от стандартных, и может использовать различные псевдонимы частоты для интервалов, подобных датам:
In [209]: pd.interval_range(start=0, periods=5, freq=1.5)
Out[209]: 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 [210]: pd.interval_range(start=pd.Timestamp("2017-01-01"), periods=4, freq="W")
Out[210]: IntervalIndex([(2017-01-01, 2017-01-08], (2017-01-08, 2017-01-15], (2017-01-15, 2017-01-22], (2017-01-22, 2017-01-29]], dtype='interval[datetime64[ns], right]')
In [211]: pd.interval_range(start=pd.Timedelta("0 days"), periods=3, freq="9H")
Out[211]: 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 [212]: pd.interval_range(start=0, end=4, closed="both")
Out[212]: IntervalIndex([[0, 1], [1, 2], [2, 3], [3, 4]], dtype='interval[int64, both]')
In [213]: pd.interval_range(start=0, end=4, closed="neither")
Out[213]: IntervalIndex([(0, 1), (1, 2), (2, 3), (3, 4)], dtype='interval[int64, neither]')
Указание start, end, и periods сгенерирует диапазон равномерно распределенных интервалов от start до end включительно с periods элементами в результирующем IntervalIndex:
In [214]: pd.interval_range(start=0, end=6, periods=4)
Out[214]: IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]], dtype='interval[float64, right]')
In [215]: pd.interval_range(pd.Timestamp("2018-01-01"), pd.Timestamp("2018-02-28"), periods=3)
Out[215]: IntervalIndex([(2018-01-01, 2018-01-20 08:00:00], (2018-01-20 08:00:00, 2018-02-08 16:00:00], (2018-02-08 16:00:00, 2018-02-28]], dtype='interval[datetime64[ns], right]')
Разное по индексированию (FAQ)
Индексирование целыми числами
Индексирование по меткам с целочисленными метками осей — непростая тема. Она активно обсуждалась на списках рассылки и среди различных членов научного сообщества Python. В pandas мы придерживаемся общей точки зрения, что метки важны больше, чем целочисленные позиции. Поэтому с целочисленным индексом оси только возможно индексирование по меткам с помощью стандартных инструментов, таких как .loc. Следующий код сгенерирует исключения:
In [216]: s = pd.Series(range(5))
In [217]: s[-1]
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
File ~/work/pandas/pandas/pandas/core/indexes/range.py:392, in RangeIndex.get_loc(self, key, method, tolerance)
391 try:
--> 392 return self._range.index(new_key)
393 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 [217], line 1
----> 1 s[-1]
File ~/work/pandas/pandas/pandas/core/series.py:982, in Series.__getitem__(self, key)
979 return self._values[key]
981 elif key_is_scalar:
--> 982 return self._get_value(key)
984 if is_hashable(key):
985 # Otherwise index.get_value will raise InvalidIndexError
986 try:
987 # For labels that don't resolve as scalars like tuples and frozensets
File ~/work/pandas/pandas/pandas/core/series.py:1092, in Series._get_value(self, label, takeable)
1089 return self._values[label]
1091 # Similar to Index.get_value, but we do not fall back to positional
-> 1092 loc = self.index.get_loc(label)
1093 return self.index._get_values_for_loc(self, loc, label)
File ~/work/pandas/pandas/pandas/core/indexes/range.py:394, in RangeIndex.get_loc(self, key, method, tolerance)
392 return self._range.index(new_key)
393 except ValueError as err:
--> 394 raise KeyError(key) from err
395 self._check_indexing_error(key)
396 raise KeyError(key)
KeyError: -1
In [218]: df = pd.DataFrame(np.random.randn(5, 4))
In [219]: df
Out[219]:
0 1 2 3
0 -0.130121 -0.476046 0.759104 0.213379
1 -0.082641 0.448008 0.656420 -1.051443
2 0.594956 -0.151360 -0.069303 1.221431
3 -0.182832 0.791235 0.042745 2.069775
4 1.446552 0.019814 -1.389212 -0.702312
In [220]: df.loc[-2:]
Out[220]:
0 1 2 3
0 -0.130121 -0.476046 0.759104 0.213379
1 -0.082641 0.448008 0.656420 -1.051443
2 0.594956 -0.151360 -0.069303 1.221431
3 -0.182832 0.791235 0.042745 2.069775
4 1.446552 0.019814 -1.389212 -0.702312
Это целенаправленное решение было принято для предотвращения неоднозначностей и скрытых ошибок (многие пользователи сообщили об ошибках, когда API был изменен, чтобы прекратить «обращение к индексированию по позиции»).
Немонотонные индексы требуют точных совпадений
Если индекс Series или DataFrame монотонно возрастает или убывает, то границы слайса по меткам могут выходить за пределы диапазона индекса, аналогично индексированию срезов в стандартном Python list. Монотонность индекса можно проверить с помощью атрибутов is_monotonic_increasing() и is_monotonic_decreasing().
In [221]: df = pd.DataFrame(index=[2, 3, 3, 4, 5], columns=["data"], data=list(range(5)))
In [222]: df.index.is_monotonic_increasing
Out[222]: True
# no rows 0 or 1, but still returns rows 2, 3 (both of them), and 4:
In [223]: df.loc[0:4, :]
Out[223]:
data
2 0
3 1
3 2
4 3
# slice is are outside the index, so empty DataFrame is returned
In [224]: df.loc[13:15, :]
Out[224]:
Empty DataFrame
Columns: [data]
Index: []
С другой стороны, если индекс не монотонный, то обе границы среза должны быть уникальными членами индекса.
In [225]: df = pd.DataFrame(index=[2, 3, 1, 4, 3, 5], columns=["data"], data=list(range(6)))
In [226]: df.index.is_monotonic_increasing
Out[226]: False
# OK because 2 and 4 are in the index
In [227]: df.loc[2:4, :]
Out[227]:
data
2 0
3 1
1 2
4 3
# 0 is not in the index
In [9]: df.loc[0:4, :]
KeyError: 0
# 3 is not a unique label
In [11]: df.loc[2:3, :]
KeyError: 'Cannot get right slice bound for non-unique label: 3'
Index.is_monotonic_increasing и Index.is_monotonic_decreasing проверяют только слабо монотонность индекса. Чтобы проверить строгую монотонность, вы можете комбинировать один из них с атрибутом is_unique().
In [228]: weakly_monotonic = pd.Index(["a", "b", "c", "c"])
In [229]: weakly_monotonic
Out[229]: Index(['a', 'b', 'c', 'c'], dtype='object')
In [230]: weakly_monotonic.is_monotonic_increasing
Out[230]: True
In [231]: weakly_monotonic.is_monotonic_increasing & weakly_monotonic.is_unique
Out[231]: False
Концы интервалов включены
В отличие от стандартных срезов в Python, где конечная точка среза не включена, индексирование по меткам в pandas включает обе конечные точки. Основная причина этого заключается в том, что часто невозможно легко определить «преемника» или следующий элемент после определенной метки в индексе. Например, рассмотрим следующий Series:
In [232]: s = pd.Series(np.random.randn(6), index=list("abcdef"))
In [233]: s
Out[233]:
a 0.301379
b 1.240445
c -0.846068
d -0.043312
e -1.658747
f -0.819549
dtype: float64
Предположим, что мы хотели бы вырезать от c до e, используя целые числа, это можно было бы сделать следующим образом:
In [234]: s[2:5]
Out[234]:
c -0.846068
d -0.043312
e -1.658747
dtype: float64
Однако, если у вас есть только c и e, определение следующего элемента в индексе может быть несколько сложным. Например, следующее не работает:
s.loc['c':'e' + 1]
Очень распространенным случаем использования является ограничение временного ряда начальной и конечной датами. Для этого мы приняли архитектурное решение сделать индексирование по меткам включающим обе конечные точки:
In [235]: s.loc["c":"e"]
Out[235]:
c -0.846068
d -0.043312
e -1.658747
dtype: float64
Это, безусловно, пример «практичность важнее чистоты», но это стоит учитывать, если вы ожидаете, что индексирование по меткам будет вести себя точно так же, как и стандартное индексирование по целым числам в Python.
Индексирование может изменить базовый тип данных Series
Различные операции индексирования могут потенциально изменить тип данных Series.
In [236]: series1 = pd.Series([1, 2, 3])
In [237]: series1.dtype
Out[237]: dtype('int64')
In [238]: res = series1.reindex([0, 4])
In [239]: res.dtype
Out[239]: dtype('float64')
In [240]: res
Out[240]:
0 1.0
4 NaN
dtype: float64
In [241]: series2 = pd.Series([True])
In [242]: series2.dtype
Out[242]: dtype('bool')
In [243]: res = series2.reindex_like(series1)
In [244]: res.dtype
Out[244]: dtype('O')
In [245]: res
Out[245]:
0 True
1 NaN
2 NaN
dtype: object
Это связано с тем, что операции (пере)индексирования выше вставляют NaNs, и тип данных dtype изменяется соответственно. Это может вызвать проблемы при использовании numpy ufuncs, таких как numpy.logical_and.
Более подробное обсуждение см. в GH2388.
© 2008–2022, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/1.5.0/user_guide/advanced.html