Spec-Zone.ru › pandas 2

Индексация и выбор данных

Информация о метках осей в объектах pandas служит нескольким целям:

  • Идентифицирует данные (то есть предоставляет метаданные) с помощью известных индикаторов, важных для анализа, визуализации и интерактивного отображения в консоли.

  • Обеспечивает автоматическое и явное выравнивание данных.

  • Позволяет интуитивно получать и задавать подмножества набора данных.

В этом разделе мы сосредоточимся на последнем пункте: рассмотрим, как нарезать данные, выбирать их части и в целом получать и задавать подмножества объектов pandas. Основное внимание будет уделено Series и DataFrame, поскольку в этой области они получили больше внимания при разработке.

Примечание

Операторы индексации Python и NumPy [] и оператор доступа к атрибутам . обеспечивают быстрый и удобный доступ к структурам данных pandas в широком спектре случаев. Благодаря этому интерактивная работа становится интуитивно понятной: если вы уже знаете, как работать со словарями Python и массивами NumPy, вам не придётся осваивать много нового. Однако, поскольку тип данных, к которому выполняется доступ, заранее неизвестен, непосредственное использование стандартных операторов имеет некоторые ограничения в плане оптимизации. В производственном коде мы рекомендуем использовать оптимизированные методы доступа к данным pandas, описанные в этой главе.

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

При операции присваивания возврат копии или ссылки может зависеть от контекста. Это иногда называют цепочечным присваиванием; его следует избегать. См. Возврат представления или копии.

Документацию по MultiIndex и более сложной индексации см. в разделе MultiIndex / Расширенная индексация.

Некоторые продвинутые стратегии описаны в сборнике рецептов.

Различные способы индексации

Для поддержки более явной индексации по местоположению в выбор объектов по запросам пользователей добавлялось множество возможностей. Теперь pandas поддерживает три типа многомерной индексации.

  • .loc в первую очередь основан на метках, но может также использоваться с булевым массивом. .loc вызовет KeyError, если элементы не найдены. Допустимые входные данные:

    • Одна метка, например 5 или 'a' (обратите внимание, что 5 интерпретируется как метка индекса. Это значение не является целочисленной позицией в индексе).

    • Список или массив меток ['a', 'b', 'c'].

    • Объект среза с метками 'a':'f' (обратите внимание: в отличие от обычных срезов Python, и начальная, и конечная границы включаются, если они присутствуют в индексе! См. разделы Срезы по меткам и Конечные точки включаются.)

    • Булев массив (любые значения NA будут рассматриваться как False).

    • Функция callable с одним аргументом (вызываемый объект Series или DataFrame), которая возвращает допустимый для индексации результат (один из перечисленных выше).

    • Кортеж индексов строк (и столбцов), элементы которого являются одним из перечисленных выше типов входных данных.

    Подробнее см. в разделе Выбор по метке.

  • .iloc в первую очередь основан на целочисленной позиции (от 0 до length-1 оси), но может также использоваться с булевым массивом. .iloc вызовет IndexError, если запрошенный индексатор выходит за границы, за исключением индексаторов типа срез, допускающих выход за границы. (Это соответствует семантике срезов Python/NumPy.) Допустимые входные данные:

    • Целое число, например 5.

    • Список или массив целых чисел [4, 3, 0].

    • Объект среза с целыми числами 1:7.

    • Булев массив (любые значения NA будут рассматриваться как False).

    • Функция callable с одним аргументом (вызываемый объект Series или DataFrame), которая возвращает допустимый для индексации результат (один из перечисленных выше).

    • Кортеж индексов строк (и столбцов), элементы которого являются одним из перечисленных выше типов входных данных.

    Подробнее см. в разделах Выбор по позиции, Расширенная индексация и Расширенная иерархическая индексация.

  • Для индексации с помощью .loc, .iloc, а также [] в качестве индексатора можно использовать callable. Подробнее см. в разделе Выбор с помощью вызываемого объекта.

    Примечание

    Распаковка ключей-кортежей в индексы строк (и столбцов) происходит до применения вызываемых объектов, поэтому вернуть кортеж из вызываемого объекта для индексации и строк, и столбцов нельзя.

Для получения значений из объекта с выбором по нескольким осям используется следующая запись (на примере .loc, но она также применима к .iloc). Любой из средств доступа к осям может быть пустым срезом :. Оси, не указанные в спецификации, считаются равными :; например, p.loc['a'] эквивалентно p.loc['a', :].

In [1]: ser = pd.Series(range(5), index=list("abcde"))

In [2]: ser.loc[["a", "c", "e"]]
Out[2]: 
a    0
c    2
e    4
dtype: int64

In [3]: df = pd.DataFrame(np.arange(25).reshape(5, 5), index=list("abcde"), columns=list("abcde"))

In [4]: df.loc[["a", "c", "e"], ["b", "d"]]
Out[4]: 
    b   d
a   1   3
c  11  13
e  21  23

Основы

Как упоминалось при знакомстве со структурами данных в предыдущем разделе, основная функция индексации с помощью [] (то есть __getitem__, если вы знакомы с реализацией поведения классов в Python) — выбор срезов меньшей размерности. В следующей таблице показаны типы возвращаемых значений при индексации объектов pandas с помощью []:

Тип объекта

Выборка

Тип возвращаемого значения

Series

series[label]

скалярное значение

DataFrame

frame[colname]

Series, соответствующий имени столбца

Для иллюстрации возможностей индексации создадим простой набор данных временного ряда:

In [5]: dates = pd.date_range('1/1/2000', periods=8)

In [6]: df = pd.DataFrame(np.random.randn(8, 4),
   ...:                   index=dates, columns=['A', 'B', 'C', 'D'])
   ...: 

In [7]: df
Out[7]: 
                   A         B         C         D
2000-01-01  0.469112 -0.282863 -1.509059 -1.135632
2000-01-02  1.212112 -0.173215  0.119209 -1.044236
2000-01-03 -0.861849 -2.104569 -0.494929  1.071804
2000-01-04  0.721555 -0.706771 -1.039575  0.271860
2000-01-05 -0.424972  0.567020  0.276232 -1.087401
2000-01-06 -0.673690  0.113648 -1.478427  0.524988
2000-01-07  0.404705  0.577046 -1.715002 -1.039268
2000-01-08 -0.370647 -1.157892 -1.344312  0.844885

Примечание

Возможности индексации не являются специфичными для временных рядов, если не указано иное.

Таким образом, как было указано выше, получаем простейшую индексацию с помощью []:

In [8]: s = df['A']

In [9]: s[dates[5]]
Out[9]: -0.6736897080883706

В [] можно передать список столбцов, чтобы выбрать их в указанном порядке. Если столбца нет в DataFrame, будет вызвано исключение. Таким же способом можно задавать значения сразу для нескольких столбцов:

In [10]: df
Out[10]: 
                   A         B         C         D
2000-01-01  0.469112 -0.282863 -1.509059 -1.135632
2000-01-02  1.212112 -0.173215  0.119209 -1.044236
2000-01-03 -0.861849 -2.104569 -0.494929  1.071804
2000-01-04  0.721555 -0.706771 -1.039575  0.271860
2000-01-05 -0.424972  0.567020  0.276232 -1.087401
2000-01-06 -0.673690  0.113648 -1.478427  0.524988
2000-01-07  0.404705  0.577046 -1.715002 -1.039268
2000-01-08 -0.370647 -1.157892 -1.344312  0.844885

In [11]: df[['B', 'A']] = df[['A', 'B']]

In [12]: df
Out[12]: 
                   A         B         C         D
2000-01-01 -0.282863  0.469112 -1.509059 -1.135632
2000-01-02 -0.173215  1.212112  0.119209 -1.044236
2000-01-03 -2.104569 -0.861849 -0.494929  1.071804
2000-01-04 -0.706771  0.721555 -1.039575  0.271860
2000-01-05  0.567020 -0.424972  0.276232 -1.087401
2000-01-06  0.113648 -0.673690 -1.478427  0.524988
2000-01-07  0.577046  0.404705 -1.715002 -1.039268
2000-01-08 -1.157892 -0.370647 -1.344312  0.844885

Это может быть полезно, если нужно применить преобразование (на месте) к подмножеству столбцов.

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

При присваивании Series и DataFrame из .loc pandas выравнивает все ОСИ.

Это не изменит df, поскольку выравнивание столбцов выполняется до присваивания значений.

In [13]: df[['A', 'B']]
Out[13]: 
                   A         B
2000-01-01 -0.282863  0.469112
2000-01-02 -0.173215  1.212112
2000-01-03 -2.104569 -0.861849
2000-01-04 -0.706771  0.721555
2000-01-05  0.567020 -0.424972
2000-01-06  0.113648 -0.673690
2000-01-07  0.577046  0.404705
2000-01-08 -1.157892 -0.370647

In [14]: df.loc[:, ['B', 'A']] = df[['A', 'B']]

In [15]: df[['A', 'B']]
Out[15]: 
                   A         B
2000-01-01 -0.282863  0.469112
2000-01-02 -0.173215  1.212112
2000-01-03 -2.104569 -0.861849
2000-01-04 -0.706771  0.721555
2000-01-05  0.567020 -0.424972
2000-01-06  0.113648 -0.673690
2000-01-07  0.577046  0.404705
2000-01-08 -1.157892 -0.370647

Правильный способ поменять местами значения столбцов — использовать исходные значения:

In [16]: df.loc[:, ['B', 'A']] = df[['A', 'B']].to_numpy()

In [17]: df[['A', 'B']]
Out[17]: 
                   A         B
2000-01-01  0.469112 -0.282863
2000-01-02  1.212112 -0.173215
2000-01-03 -0.861849 -2.104569
2000-01-04  0.721555 -0.706771
2000-01-05 -0.424972  0.567020
2000-01-06 -0.673690  0.113648
2000-01-07  0.404705  0.577046
2000-01-08 -0.370647 -1.157892

Однако pandas не выравнивает ОСИ при присваивании Series и DataFrame из .iloc, поскольку .iloc выполняет операцию по позициям.

Это изменит df, поскольку выравнивание столбцов не выполняется до присваивания значений.

In [18]: df[['A', 'B']]
Out[18]: 
                   A         B
2000-01-01  0.469112 -0.282863
2000-01-02  1.212112 -0.173215
2000-01-03 -0.861849 -2.104569
2000-01-04  0.721555 -0.706771
2000-01-05 -0.424972  0.567020
2000-01-06 -0.673690  0.113648
2000-01-07  0.404705  0.577046
2000-01-08 -0.370647 -1.157892

In [19]: df.iloc[:, [1, 0]] = df[['A', 'B']]

In [20]: df[['A','B']]
Out[20]: 
                   A         B
2000-01-01 -0.282863  0.469112
2000-01-02 -0.173215  1.212112
2000-01-03 -2.104569 -0.861849
2000-01-04 -0.706771  0.721555
2000-01-05  0.567020 -0.424972
2000-01-06  0.113648 -0.673690
2000-01-07  0.577046  0.404705
2000-01-08 -1.157892 -0.370647

Доступ к атрибутам

К индексу Series или столбцу DataFrame можно обращаться напрямую как к атрибуту:

In [21]: sa = pd.Series([1, 2, 3], index=list('abc'))

In [22]: dfa = df.copy()
In [23]: sa.b
Out[23]: 2

In [24]: dfa.A
Out[24]: 
2000-01-01   -0.282863
2000-01-02   -0.173215
2000-01-03   -2.104569
2000-01-04   -0.706771
2000-01-05    0.567020
2000-01-06    0.113648
2000-01-07    0.577046
2000-01-08   -1.157892
Freq: D, Name: A, dtype: float64
In [25]: sa.a = 5

In [26]: sa
Out[26]: 
a    5
b    2
c    3
dtype: int64

In [27]: dfa.A = list(range(len(dfa.index)))  # ok if A already exists

In [28]: dfa
Out[28]: 
            A         B         C         D
2000-01-01  0  0.469112 -1.509059 -1.135632
2000-01-02  1  1.212112  0.119209 -1.044236
2000-01-03  2 -0.861849 -0.494929  1.071804
2000-01-04  3  0.721555 -1.039575  0.271860
2000-01-05  4 -0.424972  0.276232 -1.087401
2000-01-06  5 -0.673690 -1.478427  0.524988
2000-01-07  6  0.404705 -1.715002 -1.039268
2000-01-08  7 -0.370647 -1.344312  0.844885

In [29]: dfa['A'] = list(range(len(dfa.index)))  # use this form to create a new column

In [30]: dfa
Out[30]: 
            A         B         C         D
2000-01-01  0  0.469112 -1.509059 -1.135632
2000-01-02  1  1.212112  0.119209 -1.044236
2000-01-03  2 -0.861849 -0.494929  1.071804
2000-01-04  3  0.721555 -1.039575  0.271860
2000-01-05  4 -0.424972  0.276232 -1.087401
2000-01-06  5 -0.673690 -1.478427  0.524988
2000-01-07  6  0.404705 -1.715002 -1.039268
2000-01-08  7 -0.370647 -1.344312  0.844885

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

  • Такой способ доступа можно использовать, только если элемент индекса является допустимым идентификатором Python; например, s.1 использовать нельзя. Объяснение того, что такое допустимые идентификаторы, см. здесь.

  • Атрибут будет недоступен, если его имя совпадает с именем существующего метода; например, s.min использовать нельзя, но s['min'] можно.

  • Аналогично, атрибут будет недоступен, если его имя совпадает с одним из следующих: index, major_axis, minor_axis, items.

  • Во всех этих случаях стандартная индексация по-прежнему будет работать; например, s['1'], s['min'] и s['index'] обеспечат доступ к соответствующему элементу или столбцу.

Если вы работаете в среде IPython, можно также использовать автодополнение по клавише Tab, чтобы увидеть доступные атрибуты.

Кроме того, строке DataFrame можно присвоить dict:

In [31]: x = pd.DataFrame({'x': [1, 2, 3], 'y': [3, 4, 5]})

In [32]: x.iloc[1] = {'x': 9, 'y': 99}

In [33]: x
Out[33]: 
   x   y
0  1   3
1  9  99
2  3   5

С помощью доступа к атрибутам можно изменить существующий элемент Series или столбец DataFrame, но будьте осторожны: если попытаться создать новый столбец таким способом, будет создан новый атрибут, а не столбец, и это вызовет UserWarning:

In [34]: df_new = pd.DataFrame({'one': [1., 2., 3.]})

In [35]: df_new.two = [4, 5, 6]

In [36]: df_new
Out[36]: 
   one
0  1.0
1  2.0
2  3.0

Срезы диапазонов

Наиболее надёжный и согласованный способ создавать срезы диапазонов по произвольным осям описан в разделе Выбор по позиции, посвящённом методу .iloc. Пока же мы объясним семантику срезов с помощью оператора [].

Для Series синтаксис работает так же, как и с ndarray, возвращая срез значений и соответствующие метки:

In [37]: s[:5]
Out[37]: 
2000-01-01    0.469112
2000-01-02    1.212112
2000-01-03   -0.861849
2000-01-04    0.721555
2000-01-05   -0.424972
Freq: D, Name: A, dtype: float64

In [38]: s[::2]
Out[38]: 
2000-01-01    0.469112
2000-01-03   -0.861849
2000-01-05   -0.424972
2000-01-07    0.404705
Freq: 2D, Name: A, dtype: float64

In [39]: s[::-1]
Out[39]: 
2000-01-08   -0.370647
2000-01-07    0.404705
2000-01-06   -0.673690
2000-01-05   -0.424972
2000-01-04    0.721555
2000-01-03   -0.861849
2000-01-02    1.212112
2000-01-01    0.469112
Freq: -1D, Name: A, dtype: float64

Обратите внимание, что присваивание тоже работает:

In [40]: s2 = s.copy()

In [41]: s2[:5] = 0

In [42]: s2
Out[42]: 
2000-01-01    0.000000
2000-01-02    0.000000
2000-01-03    0.000000
2000-01-04    0.000000
2000-01-05    0.000000
2000-01-06   -0.673690
2000-01-07    0.404705
2000-01-08   -0.370647
Freq: D, Name: A, dtype: float64

Для DataFrame срез внутри [] создаёт срез строк. Это реализовано главным образом для удобства, поскольку такая операция очень распространена.

In [43]: df[:3]
Out[43]: 
                   A         B         C         D
2000-01-01 -0.282863  0.469112 -1.509059 -1.135632
2000-01-02 -0.173215  1.212112  0.119209 -1.044236
2000-01-03 -2.104569 -0.861849 -0.494929  1.071804

In [44]: df[::-1]
Out[44]: 
                   A         B         C         D
2000-01-08 -1.157892 -0.370647 -1.344312  0.844885
2000-01-07  0.577046  0.404705 -1.715002 -1.039268
2000-01-06  0.113648 -0.673690 -1.478427  0.524988
2000-01-05  0.567020 -0.424972  0.276232 -1.087401
2000-01-04 -0.706771  0.721555 -1.039575  0.271860
2000-01-03 -2.104569 -0.861849 -0.494929  1.071804
2000-01-02 -0.173215  1.212112  0.119209 -1.044236
2000-01-01 -0.282863  0.469112 -1.509059 -1.135632

Выбор по метке

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

При операции присваивания возврат копии или ссылки может зависеть от контекста. Это иногда называют chained assignment; его следует избегать. См. Возврат представления или копии.

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

.loc требует строгого соответствия, если используются срезы, несовместимые (или не приводимые) к типу индекса. Например, использование целых чисел в DatetimeIndex вызовет TypeError.

In [45]: dfl = pd.DataFrame(np.random.randn(5, 4),
   ....:                    columns=list('ABCD'),
   ....:                    index=pd.date_range('20130101', periods=5))
   ....: 

In [46]: dfl
Out[46]: 
                   A         B         C         D
2013-01-01  1.075770 -0.109050  1.643563 -1.469388
2013-01-02  0.357021 -0.674600 -1.776904 -0.968914
2013-01-03 -1.294524  0.413738  0.276662 -0.472035
2013-01-04 -0.013960 -0.362543 -0.006154 -0.923061
2013-01-05  0.895717  0.805244 -1.206412  2.565646

In [47]: dfl.loc[2:3]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[47], line 1
----> 1 dfl.loc[2:3]

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

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

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

File ~/work/pandas/pandas/pandas/core/indexes/datetimes.py:682, in DatetimeIndex.slice_indexer(self, start, end, step)
    674 # GH#33146 if start and end are combinations of str and None and Index is not
    675 # monotonic, we can not use Index.slice_indexer because it does not honor the
    676 # actual elements, is only searching for start and end
    677 if (
    678     check_str_or_none(start)
    679     or check_str_or_none(end)
    680     or self.is_monotonic_increasing
    681 ):
--> 682     return Index.slice_indexer(self, start, end, step)
    684 mask = np.array(True)
    685 in_index = True

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

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

File ~/work/pandas/pandas/pandas/core/indexes/base.py:6819, in Index.get_slice_bound(self, label, side)
   6815 original_label = label
   6817 # For datetime indices label may be a string that has to be converted
   6818 # to datetime boundary according to its resolution.
-> 6819 label = self._maybe_cast_slice_bound(label, side)
   6821 # we need to look up the label
   6822 try:

File ~/work/pandas/pandas/pandas/core/indexes/datetimes.py:642, in DatetimeIndex._maybe_cast_slice_bound(self, label, side)
    637 if isinstance(label, dt.date) and not isinstance(label, dt.datetime):
    638     # Pandas supports slicing with dates, treated as datetimes at midnight.
    639     # https://github.com/pandas-dev/pandas/issues/31501
    640     label = Timestamp(label).to_pydatetime()
--> 642 label = super()._maybe_cast_slice_bound(label, side)
    643 self._data._assert_tzawareness_compat(label)
    644 return Timestamp(label)

File ~/work/pandas/pandas/pandas/core/indexes/datetimelike.py:378, in DatetimeIndexOpsMixin._maybe_cast_slice_bound(self, label, side)
    376     return lower if side == "left" else upper
    377 elif not isinstance(label, self._data._recognized_scalars):
--> 378     self._raise_invalid_indexer("slice", label)
    380 return label

File ~/work/pandas/pandas/pandas/core/indexes/base.py:4308, in Index._raise_invalid_indexer(self, form, key, reraise)
   4306 if reraise is not lib.no_default:
   4307     raise TypeError(msg) from reraise
-> 4308 raise TypeError(msg)

TypeError: cannot do slice indexing on DatetimeIndex with these indexers [2] of type int

Строковые значения в срезах могут быть приведены к типу индекса, что позволяет выполнять естественную нарезку.

In [48]: dfl.loc['20130102':'20130104']
Out[48]: 
                   A         B         C         D
2013-01-02  0.357021 -0.674600 -1.776904 -0.968914
2013-01-03 -1.294524  0.413738  0.276662 -0.472035
2013-01-04 -0.013960 -0.362543 -0.006154 -0.923061

В pandas предусмотрен набор методов для индексации исключительно по меткам. Это протокол строгого включения: каждая запрошенная метка должна присутствовать в индексе, иначе будет вызван KeyError. При создании среза начальная И конечная границы включаются, если они присутствуют в индексе. Целые числа являются допустимыми метками, но соответствуют именно метке, а не позиции.

Основным способом доступа является атрибут .loc. Допустимы следующие входные данные:

  • Одна метка, например 5 или 'a' (обратите внимание, что 5 интерпретируется как метка индекса. Это значение не является целочисленной позицией в индексе).

  • Список или массив меток ['a', 'b', 'c'].

  • Объект среза с метками 'a':'f' (обратите внимание: в отличие от обычных срезов Python, и начальная, и конечная границы включаются, если они присутствуют в индексе! См. раздел Срезы по меткам.

  • Булев массив.

  • Объект callable; см. раздел Выбор с помощью вызываемого объекта.

In [49]: s1 = pd.Series(np.random.randn(6), index=list('abcdef'))

In [50]: s1
Out[50]: 
a    1.431256
b    1.340309
c   -1.170299
d   -0.226169
e    0.410835
f    0.813850
dtype: float64

In [51]: s1.loc['c':]
Out[51]: 
c   -1.170299
d   -0.226169
e    0.410835
f    0.813850
dtype: float64

In [52]: s1.loc['b']
Out[52]: 1.3403088497993827

Обратите внимание, что присваивание тоже работает:

In [53]: s1.loc['c':] = 0

In [54]: s1
Out[54]: 
a    1.431256
b    1.340309
c    0.000000
d    0.000000
e    0.000000
f    0.000000
dtype: float64

Для DataFrame:

In [55]: df1 = pd.DataFrame(np.random.randn(6, 4),
   ....:                    index=list('abcdef'),
   ....:                    columns=list('ABCD'))
   ....: 

In [56]: df1
Out[56]: 
          A         B         C         D
a  0.132003 -0.827317 -0.076467 -1.187678
b  1.130127 -1.436737 -1.413681  1.607920
c  1.024180  0.569605  0.875906 -2.211372
d  0.974466 -2.006747 -0.410001 -0.078638
e  0.545952 -1.219217 -1.226825  0.769804
f -1.281247 -0.727707 -0.121306 -0.097883

In [57]: df1.loc[['a', 'b', 'd'], :]
Out[57]: 
          A         B         C         D
a  0.132003 -0.827317 -0.076467 -1.187678
b  1.130127 -1.436737 -1.413681  1.607920
d  0.974466 -2.006747 -0.410001 -0.078638

Доступ с помощью срезов по меткам:

In [58]: df1.loc['d':, 'A':'C']
Out[58]: 
          A         B         C
d  0.974466 -2.006747 -0.410001
e  0.545952 -1.219217 -1.226825
f -1.281247 -0.727707 -0.121306

Получение среза по метке (эквивалентно df.xs('a')):

In [59]: df1.loc['a']
Out[59]: 
A    0.132003
B   -0.827317
C   -0.076467
D   -1.187678
Name: a, dtype: float64

Получение значений с помощью булева массива:

In [60]: df1.loc['a'] > 0
Out[60]: 
A     True
B    False
C    False
D    False
Name: a, dtype: bool

In [61]: df1.loc[:, df1.loc['a'] > 0]
Out[61]: 
          A
a  0.132003
b  1.130127
c  1.024180
d  0.974466
e  0.545952
f -1.281247

Значения NA в булевом массиве распространяются как False:

In [62]: mask = pd.array([True, False, True, False, pd.NA, False], dtype="boolean")

In [63]: mask
Out[63]: 
<BooleanArray>
[True, False, True, False, <NA>, False]
Length: 6, dtype: boolean

In [64]: df1[mask]
Out[64]: 
          A         B         C         D
a  0.132003 -0.827317 -0.076467 -1.187678
c  1.024180  0.569605  0.875906 -2.211372

Явное получение значения:

# this is also equivalent to ``df1.at['a','A']``
In [65]: df1.loc['a', 'A']
Out[65]: 0.13200317033032932

Срезы по меткам

При использовании .loc со срезами, если начальная и конечная метки присутствуют в индексе, возвращаются элементы, расположенные между ними (включая сами эти метки):

In [66]: s = pd.Series(list('abcde'), index=[0, 3, 2, 5, 4])

In [67]: s.loc[3:5]
Out[67]: 
3    b
2    c
5    d
dtype: object

Если хотя бы одна из двух меток отсутствует, но индекс отсортирован и допускает сравнение с начальной и конечной метками, срез всё равно будет работать ожидаемым образом: будут выбраны метки, занимающие положение между этими двумя:

In [68]: s.sort_index()
Out[68]: 
0    a
2    c
3    b
4    e
5    d
dtype: object

In [69]: s.sort_index().loc[1:6]
Out[69]: 
2    c
3    b
4    e
5    d
dtype: object

Однако если хотя бы одна из двух меток отсутствует и индекс не отсортирован, будет вызвана ошибка (поскольку в противном случае операция была бы вычислительно затратной, а для индексов со смешанными типами — потенциально неоднозначной). Например, в приведённом выше примере s.loc[1:6] вызовет KeyError.

Обоснование такого поведения см. в разделе Конечные точки включаются.

In [70]: s = pd.Series(list('abcdef'), index=[0, 3, 2, 5, 4, 2])

In [71]: s.loc[3:5]
Out[71]: 
3    b
2    c
5    d
dtype: object

Кроме того, если индекс содержит дублирующиеся метки и начальная или конечная метка дублируется, будет вызвана ошибка. Например, в приведённом выше примере s.loc[2:5] вызовет KeyError.

Дополнительную информацию о дублирующихся метках см. в разделе Дублирующиеся метки.

Выбор по позиции

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

При операции присваивания возврат копии или ссылки может зависеть от контекста. Это иногда называют chained assignment; его следует избегать. См. Возврат представления или копии.

В pandas предусмотрен набор методов для индексации исключительно по целым числам. Семантика близка к нарезке в Python и NumPy. Эти методы выполняют индексацию 0-based. При создании среза начальная граница включается, а верхняя граница не включается. Попытка использовать нецелое число, даже допустимую метку, вызовет IndexError.

Основным способом доступа является атрибут .iloc. Допустимы следующие входные данные:

  • Целое число, например 5.

  • Список или массив целых чисел [4, 3, 0].

  • Объект среза с целыми числами 1:7.

  • Булев массив.

  • Объект callable; см. раздел Выбор с помощью вызываемого объекта.

  • Кортеж индексов строк (и столбцов), элементы которого относятся к одному из перечисленных выше типов.

In [72]: s1 = pd.Series(np.random.randn(5), index=list(range(0, 10, 2)))

In [73]: s1
Out[73]: 
0    0.695775
2    0.341734
4    0.959726
6   -1.110336
8   -0.619976
dtype: float64

In [74]: s1.iloc[:3]
Out[74]: 
0    0.695775
2    0.341734
4    0.959726
dtype: float64

In [75]: s1.iloc[3]
Out[75]: -1.110336102891167

Обратите внимание, что присваивание тоже работает:

In [76]: s1.iloc[:3] = 0

In [77]: s1
Out[77]: 
0    0.000000
2    0.000000
4    0.000000
6   -1.110336
8   -0.619976
dtype: float64

Для DataFrame:

In [78]: df1 = pd.DataFrame(np.random.randn(6, 4),
   ....:                    index=list(range(0, 12, 2)),
   ....:                    columns=list(range(0, 8, 2)))
   ....: 

In [79]: df1
Out[79]: 
           0         2         4         6
0   0.149748 -0.732339  0.687738  0.176444
2   0.403310 -0.154951  0.301624 -2.179861
4  -1.369849 -0.954208  1.462696 -1.743161
6  -0.826591 -0.345352  1.314232  0.690579
8   0.995761  2.396780  0.014871  3.357427
10 -0.317441 -1.236269  0.896171 -0.487602

Выбор с помощью целочисленного среза:

In [80]: df1.iloc[:3]
Out[80]: 
          0         2         4         6
0  0.149748 -0.732339  0.687738  0.176444
2  0.403310 -0.154951  0.301624 -2.179861
4 -1.369849 -0.954208  1.462696 -1.743161

In [81]: df1.iloc[1:5, 2:4]
Out[81]: 
          4         6
2  0.301624 -2.179861
4  1.462696 -1.743161
6  1.314232  0.690579
8  0.014871  3.357427

Выбор с помощью списка целых чисел:

In [82]: df1.iloc[[1, 3, 5], [1, 3]]
Out[82]: 
           2         6
2  -0.154951 -2.179861
6  -0.345352  0.690579
10 -1.236269 -0.487602
In [83]: df1.iloc[1:3, :]
Out[83]: 
          0         2         4         6
2  0.403310 -0.154951  0.301624 -2.179861
4 -1.369849 -0.954208  1.462696 -1.743161
In [84]: df1.iloc[:, 1:3]
Out[84]: 
           2         4
0  -0.732339  0.687738
2  -0.154951  0.301624
4  -0.954208  1.462696
6  -0.345352  1.314232
8   2.396780  0.014871
10 -1.236269  0.896171
# this is also equivalent to ``df1.iat[1,1]``
In [85]: df1.iloc[1, 1]
Out[85]: -0.1549507744249032

Получение среза по целочисленной позиции (эквивалентно df.xs(1)):

In [86]: df1.iloc[1]
Out[86]: 
0    0.403310
2   -0.154951
4    0.301624
6   -2.179861
Name: 2, dtype: float64

Индексы среза, выходящие за границы, обрабатываются корректно, как в Python/NumPy.

# these are allowed in Python/NumPy.
In [87]: x = list('abcdef')

In [88]: x
Out[88]: ['a', 'b', 'c', 'd', 'e', 'f']

In [89]: x[4:10]
Out[89]: ['e', 'f']

In [90]: x[8:10]
Out[90]: []

In [91]: s = pd.Series(x)

In [92]: s
Out[92]: 
0    a
1    b
2    c
3    d
4    e
5    f
dtype: object

In [93]: s.iloc[4:10]
Out[93]: 
4    e
5    f
dtype: object

In [94]: s.iloc[8:10]
Out[94]: Series([], dtype: object)

Обратите внимание: срезы, выходящие за границы, могут привести к пустой оси (например, будет возвращён пустой DataFrame).

In [95]: dfl = pd.DataFrame(np.random.randn(5, 2), columns=list('AB'))

In [96]: dfl
Out[96]: 
          A         B
0 -0.082240 -2.182937
1  0.380396  0.084844
2  0.432390  1.519970
3 -0.493662  0.600178
4  0.274230  0.132885

In [97]: dfl.iloc[:, 2:3]
Out[97]: 
Empty DataFrame
Columns: []
Index: [0, 1, 2, 3, 4]

In [98]: dfl.iloc[:, 1:3]
Out[98]: 
          B
0 -2.182937
1  0.084844
2  1.519970
3  0.600178
4  0.132885

In [99]: dfl.iloc[4:6]
Out[99]: 
         A         B
4  0.27423  0.132885

Если один индексатор выходит за границы, будет вызван IndexError. Если хотя бы один элемент списка индексаторов выходит за границы, будет вызван IndexError.

In [100]: dfl.iloc[[4, 5, 6]]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
File ~/work/pandas/pandas/pandas/core/indexing.py:1714, in _iLocIndexer._get_list_axis(self, key, axis)
   1713 try:
-> 1714     return self.obj._take_with_is_copy(key, axis=axis)
   1715 except IndexError as err:
   1716     # re-raise with different error message, e.g. test_getitem_ndarray_3d

File ~/work/pandas/pandas/pandas/core/generic.py:4172, in NDFrame._take_with_is_copy(self, indices, axis)
   4163 """
   4164 Internal version of the `take` method that sets the `_is_copy`
   4165 attribute to keep track of the parent dataframe (using in indexing
   (...)
   4170 See the docstring of `take` for full explanation of the parameters.
   4171 """
-> 4172 result = self.take(indices=indices, axis=axis)
   4173 # Maybe set copy if we didn't actually change the index.

File ~/work/pandas/pandas/pandas/core/generic.py:4152, in NDFrame.take(self, indices, axis, **kwargs)
   4148     indices = np.arange(
   4149         indices.start, indices.stop, indices.step, dtype=np.intp
   4150     )
-> 4152 new_data = self._mgr.take(
   4153     indices,
   4154     axis=self._get_block_manager_axis(axis),
   4155     verify=True,
   4156 )
   4157 return self._constructor_from_mgr(new_data, axes=new_data.axes).__finalize__(
   4158     self, method="take"
   4159 )

File ~/work/pandas/pandas/pandas/core/internals/managers.py:891, in BaseBlockManager.take(self, indexer, axis, verify)
    890 n = self.shape[axis]
--> 891 indexer = maybe_convert_indices(indexer, n, verify=verify)
    893 new_labels = self.axes[axis].take(indexer)

File ~/work/pandas/pandas/pandas/core/indexers/utils.py:282, in maybe_convert_indices(indices, n, verify)
    281     if mask.any():
--> 282         raise IndexError("indices are out-of-bounds")
    283 return indices

IndexError: indices are out-of-bounds

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

IndexError                                Traceback (most recent call last)
Cell In[100], line 1
----> 1 dfl.iloc[[4, 5, 6]]

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

File ~/work/pandas/pandas/pandas/core/indexing.py:1743, in _iLocIndexer._getitem_axis(self, key, axis)
   1741 # a list of integers
   1742 elif is_list_like_indexer(key):
-> 1743     return self._get_list_axis(key, axis=axis)
   1745 # a single integer
   1746 else:
   1747     key = item_from_zerodim(key)

File ~/work/pandas/pandas/pandas/core/indexing.py:1717, in _iLocIndexer._get_list_axis(self, key, axis)
   1714     return self.obj._take_with_is_copy(key, axis=axis)
   1715 except IndexError as err:
   1716     # re-raise with different error message, e.g. test_getitem_ndarray_3d
-> 1717     raise IndexError("positional indexers are out-of-bounds") from err

IndexError: positional indexers are out-of-bounds
In [101]: dfl.iloc[:, 4]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[101], line 1
----> 1 dfl.iloc[:, 4]

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

File ~/work/pandas/pandas/pandas/core/indexing.py:1690, in _iLocIndexer._getitem_tuple(self, tup)
   1689 def _getitem_tuple(self, tup: tuple):
-> 1690     tup = self._validate_tuple_indexer(tup)
   1691     with suppress(IndexingError):
   1692         return self._getitem_lowerdim(tup)

File ~/work/pandas/pandas/pandas/core/indexing.py:966, in _LocationIndexer._validate_tuple_indexer(self, key)
    964 for i, k in enumerate(key):
    965     try:
--> 966         self._validate_key(k, i)
    967     except ValueError as err:
    968         raise ValueError(
    969             "Location based indexing can only have "
    970             f"[{self._valid_types}] types"
    971         ) from err

File ~/work/pandas/pandas/pandas/core/indexing.py:1592, in _iLocIndexer._validate_key(self, key, axis)
   1590     return
   1591 elif is_integer(key):
-> 1592     self._validate_integer(key, axis)
   1593 elif isinstance(key, tuple):
   1594     # a tuple should already have been caught by this point
   1595     # so don't treat a tuple as a valid indexer
   1596     raise IndexingError("Too many indexers")

File ~/work/pandas/pandas/pandas/core/indexing.py:1685, in _iLocIndexer._validate_integer(self, key, axis)
   1683 len_axis = len(self.obj._get_axis(axis))
   1684 if key >= len_axis or key < -len_axis:
-> 1685     raise IndexError("single positional indexer is out-of-bounds")

IndexError: single positional indexer is out-of-bounds

Выбор с помощью вызываемого объекта

Для индексации с помощью .loc, .iloc, а также [] в качестве индексатора можно использовать callable. callable должна быть функцией с одним аргументом (вызываемый объект Series или DataFrame), возвращающей допустимый для индексации результат.

Примечание

Для индексации с помощью .iloc возврат кортежа из вызываемого объекта не поддерживается, поскольку распаковка кортежа в индексы строк и столбцов выполняется до применения вызываемых объектов.

In [102]: df1 = pd.DataFrame(np.random.randn(6, 4),
   .....:                    index=list('abcdef'),
   .....:                    columns=list('ABCD'))
   .....: 

In [103]: df1
Out[103]: 
          A         B         C         D
a -0.023688  2.410179  1.450520  0.206053
b -0.251905 -2.213588  1.063327  1.266143
c  0.299368 -0.863838  0.408204 -1.048089
d -0.025747 -0.988387  0.094055  1.262731
e  1.289997  0.082423 -0.055758  0.536580
f -0.489682  0.369374 -0.034571 -2.484478

In [104]: df1.loc[lambda df: df['A'] > 0, :]
Out[104]: 
          A         B         C         D
c  0.299368 -0.863838  0.408204 -1.048089
e  1.289997  0.082423 -0.055758  0.536580

In [105]: df1.loc[:, lambda df: ['A', 'B']]
Out[105]: 
          A         B
a -0.023688  2.410179
b -0.251905 -2.213588
c  0.299368 -0.863838
d -0.025747 -0.988387
e  1.289997  0.082423
f -0.489682  0.369374

In [106]: df1.iloc[:, lambda df: [0, 1]]
Out[106]: 
          A         B
a -0.023688  2.410179
b -0.251905 -2.213588
c  0.299368 -0.863838
d -0.025747 -0.988387
e  1.289997  0.082423
f -0.489682  0.369374

In [107]: df1[lambda df: df.columns[0]]
Out[107]: 
a   -0.023688
b   -0.251905
c    0.299368
d   -0.025747
e    1.289997
f   -0.489682
Name: A, dtype: float64

Индексацию с помощью вызываемого объекта можно использовать в Series.

In [108]: df1['A'].loc[lambda s: s > 0]
Out[108]: 
c    0.299368
e    1.289997
Name: A, dtype: float64

С помощью этих методов и индексаторов можно объединять операции выбора данных в цепочки, не используя временную переменную.

In [109]: bb = pd.read_csv('data/baseball.csv', index_col='id')

In [110]: (bb.groupby(['year', 'team']).sum(numeric_only=True)
   .....:    .loc[lambda df: df['r'] > 100])
   .....: 
Out[110]: 
           stint    g    ab    r    h  X2b  ...     so   ibb   hbp    sh    sf  gidp
year team                                   ...                                     
2007 CIN       6  379   745  101  203   35  ...  127.0  14.0   1.0   1.0  15.0  18.0
     DET       5  301  1062  162  283   54  ...  176.0   3.0  10.0   4.0   8.0  28.0
     HOU       4  311   926  109  218   47  ...  212.0   3.0   9.0  16.0   6.0  17.0
     LAN      11  413  1021  153  293   61  ...  141.0   8.0   9.0   3.0   8.0  29.0
     NYN      13  622  1854  240  509  101  ...  310.0  24.0  23.0  18.0  15.0  48.0
     SFN       5  482  1305  198  337   67  ...  188.0  51.0   8.0  16.0   6.0  41.0
     TEX       2  198   729  115  200   40  ...  140.0   4.0   5.0   2.0   8.0  16.0
     TOR       4  459  1408  187  378   96  ...  265.0  16.0  12.0   4.0  16.0  38.0

[8 rows x 18 columns]

Сочетание индексации по позиции и по меткам

Если нужно получить 0-й и 2-й элементы индекса в столбце «A», можно сделать следующее:

In [111]: dfd = pd.DataFrame({'A': [1, 2, 3],
   .....:                     'B': [4, 5, 6]},
   .....:                    index=list('abc'))
   .....: 

In [112]: dfd
Out[112]: 
   A  B
a  1  4
b  2  5
c  3  6

In [113]: dfd.loc[dfd.index[[0, 2]], 'A']
Out[113]: 
a    1
c    3
Name: A, dtype: int64

Это также можно выразить с помощью .iloc: явно получить позиции в индексаторах, а затем выбрать элементы с помощью индексации по позициям.

In [114]: dfd.iloc[[0, 2], dfd.columns.get_loc('A')]
Out[114]: 
a    1
c    3
Name: A, dtype: int64

Чтобы получить несколько индексаторов, используйте .get_indexer:

In [115]: dfd.iloc[[0, 2], dfd.columns.get_indexer(['A', 'B'])]
Out[115]: 
   A  B
a  1  4
c  3  6

Переиндексация

Идиоматичный способ выбрать элементы, которые могут отсутствовать, — использовать .reindex(). См. также раздел о переиндексации.

In [116]: s = pd.Series([1, 2, 3])

In [117]: s.reindex([1, 2, 3])
Out[117]: 
1    2.0
2    3.0
3    NaN
dtype: float64

В качестве альтернативы, если нужно выбрать только допустимые ключи, следующий способ является идиоматичным и эффективным; он гарантированно сохраняет dtype выборки.

In [118]: labels = [1, 2, 3]

In [119]: s.loc[s.index.intersection(labels)]
Out[119]: 
1    2
2    3
dtype: int64

Дублирующийся индекс вызовет ошибку при использовании .reindex():

In [120]: s = pd.Series(np.arange(4), index=['a', 'a', 'b', 'c'])

In [121]: labels = ['c', 'd']

In [122]: s.reindex(labels)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[122], line 1
----> 1 s.reindex(labels)

File ~/work/pandas/pandas/pandas/core/series.py:5164, in Series.reindex(self, index, axis, method, copy, level, fill_value, limit, tolerance)
   5147 @doc(
   5148     NDFrame.reindex,  # type: ignore[has-type]
   5149     klass=_shared_doc_kwargs["klass"],
   (...)
   5162     tolerance=None,
   5163 ) -> Series:
-> 5164     return super().reindex(
   5165         index=index,
   5166         method=method,
   5167         copy=copy,
   5168         level=level,
   5169         fill_value=fill_value,
   5170         limit=limit,
   5171         tolerance=tolerance,
   5172     )

File ~/work/pandas/pandas/pandas/core/generic.py:5629, in NDFrame.reindex(self, labels, index, columns, axis, method, copy, level, fill_value, limit, tolerance)
   5626     return self._reindex_multi(axes, copy, fill_value)
   5628 # perform the reindex on the axes
-> 5629 return self._reindex_axes(
   5630     axes, level, limit, tolerance, method, fill_value, copy
   5631 ).__finalize__(self, method="reindex")

File ~/work/pandas/pandas/pandas/core/generic.py:5652, in NDFrame._reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
   5649     continue
   5651 ax = self._get_axis(a)
-> 5652 new_index, indexer = ax.reindex(
   5653     labels, level=level, limit=limit, tolerance=tolerance, method=method
   5654 )
   5656 axis = self._get_axis_number(a)
   5657 obj = obj._reindex_with_indexers(
   5658     {axis: [new_index, indexer]},
   5659     fill_value=fill_value,
   5660     copy=copy,
   5661     allow_dups=False,
   5662 )

File ~/work/pandas/pandas/pandas/core/indexes/base.py:4436, in Index.reindex(self, target, method, level, limit, tolerance)
   4433     raise ValueError("cannot handle a non-unique multi-index!")
   4434 elif not self.is_unique:
   4435     # GH#42568
-> 4436     raise ValueError("cannot reindex on an axis with duplicate labels")
   4437 else:
   4438     indexer, _ = self.get_indexer_non_unique(target)

ValueError: cannot reindex on an axis with duplicate labels

Как правило, можно пересечь нужные метки с текущей осью, а затем выполнить переиндексацию.

In [123]: s.loc[s.index.intersection(labels)].reindex(labels)
Out[123]: 
c    3.0
d    NaN
dtype: float64

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

In [124]: labels = ['a', 'd']

In [125]: s.loc[s.index.intersection(labels)].reindex(labels)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[125], line 1
----> 1 s.loc[s.index.intersection(labels)].reindex(labels)

File ~/work/pandas/pandas/pandas/core/series.py:5164, in Series.reindex(self, index, axis, method, copy, level, fill_value, limit, tolerance)
   5147 @doc(
   5148     NDFrame.reindex,  # type: ignore[has-type]
   5149     klass=_shared_doc_kwargs["klass"],
   (...)
   5162     tolerance=None,
   5163 ) -> Series:
-> 5164     return super().reindex(
   5165         index=index,
   5166         method=method,
   5167         copy=copy,
   5168         level=level,
   5169         fill_value=fill_value,
   5170         limit=limit,
   5171         tolerance=tolerance,
   5172     )

File ~/work/pandas/pandas/pandas/core/generic.py:5629, in NDFrame.reindex(self, labels, index, columns, axis, method, copy, level, fill_value, limit, tolerance)
   5626     return self._reindex_multi(axes, copy, fill_value)
   5628 # perform the reindex on the axes
-> 5629 return self._reindex_axes(
   5630     axes, level, limit, tolerance, method, fill_value, copy
   5631 ).__finalize__(self, method="reindex")

File ~/work/pandas/pandas/pandas/core/generic.py:5652, in NDFrame._reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
   5649     continue
   5651 ax = self._get_axis(a)
-> 5652 new_index, indexer = ax.reindex(
   5653     labels, level=level, limit=limit, tolerance=tolerance, method=method
   5654 )
   5656 axis = self._get_axis_number(a)
   5657 obj = obj._reindex_with_indexers(
   5658     {axis: [new_index, indexer]},
   5659     fill_value=fill_value,
   5660     copy=copy,
   5661     allow_dups=False,
   5662 )

File ~/work/pandas/pandas/pandas/core/indexes/base.py:4436, in Index.reindex(self, target, method, level, limit, tolerance)
   4433     raise ValueError("cannot handle a non-unique multi-index!")
   4434 elif not self.is_unique:
   4435     # GH#42568
-> 4436     raise ValueError("cannot reindex on an axis with duplicate labels")
   4437 else:
   4438     indexer, _ = self.get_indexer_non_unique(target)

ValueError: cannot reindex on an axis with duplicate labels

Выбор случайных выборок

Случайную выборку строк или столбцов из Series или DataFrame можно получить с помощью метода sample(). По умолчанию метод выбирает строки; он принимает конкретное число строк или столбцов для возврата либо долю строк.

In [126]: s = pd.Series([0, 1, 2, 3, 4, 5])

# When no arguments are passed, returns 1 row.
In [127]: s.sample()
Out[127]: 
4    4
dtype: int64

# One may specify either a number of rows:
In [128]: s.sample(n=3)
Out[128]: 
0    0
4    4
1    1
dtype: int64

# Or a fraction of the rows:
In [129]: s.sample(frac=0.5)
Out[129]: 
5    5
3    3
1    1
dtype: int64

По умолчанию sample возвращает каждую строку не более одного раза, однако можно выполнять выборку с возвращением, используя параметр replace:

In [130]: s = pd.Series([0, 1, 2, 3, 4, 5])

# Without replacement (default):
In [131]: s.sample(n=6, replace=False)
Out[131]: 
0    0
1    1
5    5
3    3
2    2
4    4
dtype: int64

# With replacement:
In [132]: s.sample(n=6, replace=True)
Out[132]: 
0    0
4    4
3    3
2    2
4    4
4    4
dtype: int64

По умолчанию вероятность выбора каждой строки одинакова. Если нужны разные вероятности для разных строк, можно передать веса выборки функции sample в виде weights. Веса могут быть списком, массивом NumPy или Series, но их длина должна совпадать с длиной объекта, из которого выполняется выборка. Пропущенные значения будут считаться весом, равным нулю, а бесконечные значения не допускаются. Если сумма весов не равна 1, они будут нормализованы повторно путём деления каждого веса на сумму весов. Например:

In [133]: s = pd.Series([0, 1, 2, 3, 4, 5])

In [134]: example_weights = [0, 0, 0.2, 0.2, 0.2, 0.4]

In [135]: s.sample(n=3, weights=example_weights)
Out[135]: 
5    5
4    4
3    3
dtype: int64

# Weights will be re-normalized automatically
In [136]: example_weights2 = [0.5, 0, 0, 0, 0, 0]

In [137]: s.sample(n=1, weights=example_weights2)
Out[137]: 
0    0
dtype: int64

При применении к DataFrame в качестве весов выборки можно использовать столбец DataFrame (если выбираются строки, а не столбцы), просто передав имя столбца в виде строки.

In [138]: df2 = pd.DataFrame({'col1': [9, 8, 7, 6],
   .....:                     'weight_column': [0.5, 0.4, 0.1, 0]})
   .....: 

In [139]: df2.sample(n=3, weights='weight_column')
Out[139]: 
   col1  weight_column
1     8            0.4
0     9            0.5
2     7            0.1

Кроме того, sample позволяет выбирать столбцы, а не строки, с помощью аргумента axis.

In [140]: df3 = pd.DataFrame({'col1': [1, 2, 3], 'col2': [2, 3, 4]})

In [141]: df3.sample(n=1, axis=1)
Out[141]: 
   col1
0     1
1     2
2     3

Наконец, можно задать начальное значение для генератора случайных чисел sample с помощью аргумента random_state, принимающего целое число (в качестве начального значения) или объект RandomState из NumPy.

In [142]: df4 = pd.DataFrame({'col1': [1, 2, 3], 'col2': [2, 3, 4]})

# With a given seed, the sample will always draw the same rows.
In [143]: df4.sample(n=2, random_state=2)
Out[143]: 
   col1  col2
2     3     4
1     2     3

In [144]: df4.sample(n=2, random_state=2)
Out[144]: 
   col1  col2
2     3     4
1     2     3

Присваивание с расширением

Операции .loc/[] могут расширять объект, если при присваивании для этой оси указан несуществующий ключ.

В случае Series это фактически операция добавления.

In [145]: se = pd.Series([1, 2, 3])

In [146]: se
Out[146]: 
0    1
1    2
2    3
dtype: int64

In [147]: se[5] = 5.

In [148]: se
Out[148]: 
0    1.0
1    2.0
2    3.0
5    5.0
dtype: float64

Объект DataFrame можно расширить по любой из осей с помощью .loc.

In [149]: dfi = pd.DataFrame(np.arange(6).reshape(3, 2),
   .....:                    columns=['A', 'B'])
   .....: 

In [150]: dfi
Out[150]: 
   A  B
0  0  1
1  2  3
2  4  5

In [151]: dfi.loc[:, 'C'] = dfi.loc[:, 'A']

In [152]: dfi
Out[152]: 
   A  B  C
0  0  1  0
1  2  3  2
2  4  5  4

Это похоже на операцию append для DataFrame.

In [153]: dfi.loc[3] = 5

In [154]: dfi
Out[154]: 
   A  B  C
0  0  1  0
1  2  3  2
2  4  5  4
3  5  5  5

Быстрое получение и присваивание скалярных значений

Индексация с помощью [] должна обрабатывать множество случаев (доступ по одной метке, срезы, булева индексация и т. д.), поэтому ей требуется время на определение того, что именно вы запрашиваете. Если нужен доступ только к скалярному значению, быстрее всего использовать методы at и iat, реализованные для всех структур данных.

Подобно loc, at обеспечивает скалярный доступ по метке, тогда как iat обеспечивает доступ по целочисленной позиции, аналогично iloc.

In [155]: s.iat[5]
Out[155]: 5

In [156]: df.at[dates[5], 'A']
Out[156]: 0.1136484096888855

In [157]: df.iat[3, 0]
Out[157]: -0.7067711336300845

Эти же индексаторы можно использовать и для присваивания.

In [158]: df.at[dates[5], 'E'] = 7

In [159]: df.iat[3, 0] = 7

Как и в примере выше, at может расширить объект на месте, если индексатор отсутствует.

In [160]: df.at[dates[-1] + pd.Timedelta('1 day'), 0] = 7

In [161]: df
Out[161]: 
                   A         B         C         D    E    0
2000-01-01 -0.282863  0.469112 -1.509059 -1.135632  NaN  NaN
2000-01-02 -0.173215  1.212112  0.119209 -1.044236  NaN  NaN
2000-01-03 -2.104569 -0.861849 -0.494929  1.071804  NaN  NaN
2000-01-04  7.000000  0.721555 -1.039575  0.271860  NaN  NaN
2000-01-05  0.567020 -0.424972  0.276232 -1.087401  NaN  NaN
2000-01-06  0.113648 -0.673690 -1.478427  0.524988  7.0  NaN
2000-01-07  0.577046  0.404705 -1.715002 -1.039268  NaN  NaN
2000-01-08 -1.157892 -0.370647 -1.344312  0.844885  NaN  NaN
2000-01-09       NaN       NaN       NaN       NaN  NaN  7.0

Булева индексация

Ещё одна распространённая операция — использование булевых векторов для фильтрации данных. Операторы: | для or, & для and и ~ для not. Их необходимо группировать с помощью скобок, поскольку по умолчанию Python вычисляет выражение, например df['A'] > 2 & df['B'] < 3, как df['A'] > (2 & df['B']) < 3, тогда как желаемый порядок вычисления — (df['A'] > 2) & (df['B'] < 3).

Индексация Series с помощью булева вектора работает точно так же, как для массива NumPy ndarray:

In [162]: s = pd.Series(range(-3, 4))

In [163]: s
Out[163]: 
0   -3
1   -2
2   -1
3    0
4    1
5    2
6    3
dtype: int64

In [164]: s[s > 0]
Out[164]: 
4    1
5    2
6    3
dtype: int64

In [165]: s[(s < -1) | (s > 0.5)]
Out[165]: 
0   -3
1   -2
4    1
5    2
6    3
dtype: int64

In [166]: s[~(s < 0)]
Out[166]: 
3    0
4    1
5    2
6    3
dtype: int64

Вы можете выбирать строки из DataFrame с помощью булева вектора той же длины, что и индекс DataFrame (например, вектора, полученного из одного из столбцов DataFrame):

In [167]: df[df['A'] > 0]
Out[167]: 
                   A         B         C         D    E   0
2000-01-04  7.000000  0.721555 -1.039575  0.271860  NaN NaN
2000-01-05  0.567020 -0.424972  0.276232 -1.087401  NaN NaN
2000-01-06  0.113648 -0.673690 -1.478427  0.524988  7.0 NaN
2000-01-07  0.577046  0.404705 -1.715002 -1.039268  NaN NaN

Для задания более сложных критериев также можно использовать включения списков и метод map класса Series:

In [168]: df2 = pd.DataFrame({'a': ['one', 'one', 'two', 'three', 'two', 'one', 'six'],
   .....:                     'b': ['x', 'y', 'y', 'x', 'y', 'x', 'x'],
   .....:                     'c': np.random.randn(7)})
   .....: 

# only want 'two' or 'three'
In [169]: criterion = df2['a'].map(lambda x: x.startswith('t'))

In [170]: df2[criterion]
Out[170]: 
       a  b         c
2    two  y  0.041290
3  three  x  0.361719
4    two  y -0.238075

# equivalent but slower
In [171]: df2[[x.startswith('t') for x in df2['a']]]
Out[171]: 
       a  b         c
2    two  y  0.041290
3  three  x  0.361719
4    two  y -0.238075

# Multiple criteria
In [172]: df2[criterion & (df2['b'] == 'x')]
Out[172]: 
       a  b         c
3  three  x  0.361719

С помощью методов выбора Выбор по метке, Выбор по позиции и Расширенная индексация можно выбирать данные по нескольким осям, используя булевы векторы в сочетании с другими выражениями индексации.

In [173]: df2.loc[criterion & (df2['b'] == 'x'), 'b':'c']
Out[173]: 
   b         c
3  x  0.361719

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

iloc поддерживает два вида булевой индексации. Если индексатор — булев Series, будет вызвана ошибка. Например, в следующем примере df.iloc[s.values, 1] допустимо. Булев индексатор представляет собой массив. Однако df.iloc[s, 1] вызовет ValueError.

In [174]: df = pd.DataFrame([[1, 2], [3, 4], [5, 6]],
   .....:                   index=list('abc'),
   .....:                   columns=['A', 'B'])
   .....: 

In [175]: s = (df['A'] > 2)

In [176]: s
Out[176]: 
a    False
b     True
c     True
Name: A, dtype: bool

In [177]: df.loc[s, 'B']
Out[177]: 
b    4
c    6
Name: B, dtype: int64

In [178]: df.iloc[s.values, 1]
Out[178]: 
b    4
c    6
Name: B, dtype: int64

Индексация с помощью isin

Рассмотрим метод isin() класса Series, который возвращает булев вектор со значением True для элементов Series, присутствующих в переданном списке. Это позволяет выбирать строки, в которых один или несколько столбцов содержат нужные значения:

In [179]: s = pd.Series(np.arange(5), index=np.arange(5)[::-1], dtype='int64')

In [180]: s
Out[180]: 
4    0
3    1
2    2
1    3
0    4
dtype: int64

In [181]: s.isin([2, 4, 6])
Out[181]: 
4    False
3    False
2     True
1    False
0     True
dtype: bool

In [182]: s[s.isin([2, 4, 6])]
Out[182]: 
2    2
0    4
dtype: int64

Этот же метод доступен для объектов Index и полезен в случаях, когда неизвестно, какие из искомых меток действительно присутствуют:

In [183]: s[s.index.isin([2, 4, 6])]
Out[183]: 
4    0
2    2
dtype: int64

# compare it to the following
In [184]: s.reindex([2, 4, 6])
Out[184]: 
2    2.0
4    0.0
6    NaN
dtype: float64

Кроме того, MultiIndex позволяет выбрать отдельный уровень для проверки принадлежности:

In [185]: s_mi = pd.Series(np.arange(6),
   .....:                  index=pd.MultiIndex.from_product([[0, 1], ['a', 'b', 'c']]))
   .....: 

In [186]: s_mi
Out[186]: 
0  a    0
   b    1
   c    2
1  a    3
   b    4
   c    5
dtype: int64

In [187]: s_mi.iloc[s_mi.index.isin([(1, 'a'), (2, 'b'), (0, 'c')])]
Out[187]: 
0  c    2
1  a    3
dtype: int64

In [188]: s_mi.iloc[s_mi.index.isin(['a', 'c', 'e'], level=1)]
Out[188]: 
0  a    0
   c    2
1  a    3
   c    5
dtype: int64

У DataFrame также есть метод isin(). При вызове isin передайте набор значений в виде массива или словаря. Если values — это массив, isin возвращает DataFrame булевых значений той же формы, что и исходный DataFrame, со значением True там, где элемент входит в последовательность значений.

In [189]: df = pd.DataFrame({'vals': [1, 2, 3, 4], 'ids': ['a', 'b', 'f', 'n'],
   .....:                    'ids2': ['a', 'n', 'c', 'n']})
   .....: 

In [190]: values = ['a', 'b', 1, 3]

In [191]: df.isin(values)
Out[191]: 
    vals    ids   ids2
0   True   True   True
1  False   True  False
2   True  False  False
3  False  False  False

Часто требуется сопоставить определённые значения с определёнными столбцами. Для этого достаточно задать values как dict, где ключом является столбец, а значением — список элементов для проверки.

In [192]: values = {'ids': ['a', 'b'], 'vals': [1, 3]}

In [193]: df.isin(values)
Out[193]: 
    vals    ids   ids2
0   True   True  False
1  False   True  False
2   True  False  False
3  False  False  False

Чтобы вернуть DataFrame из булевых значений, в котором значения не входят в исходный DataFrame, используйте оператор ~:

In [194]: values = {'ids': ['a', 'b'], 'vals': [1, 3]}

In [195]: ~df.isin(values)
Out[195]: 
    vals    ids  ids2
0  False  False  True
1   True  False  True
2  False   True  True
3   True   True  True

Сочетайте isin DataFrame с методами any() и all(), чтобы быстро выбирать подмножества данных, удовлетворяющие заданному критерию. Чтобы выбрать строку, в которой каждый столбец удовлетворяет собственному критерию:

In [196]: values = {'ids': ['a', 'b'], 'ids2': ['a', 'c'], 'vals': [1, 3]}

In [197]: row_mask = df.isin(values).all(1)

In [198]: df[row_mask]
Out[198]: 
   vals ids ids2
0     1   a    a

Метод where() и маскирование

Выбор значений из Series с помощью булева вектора обычно возвращает подмножество данных. Чтобы гарантировать, что результат выборки будет иметь ту же форму, что и исходные данные, можно использовать метод where в Series и DataFrame.

Чтобы вернуть только выбранные строки:

In [199]: s[s > 0]
Out[199]: 
3    1
2    2
1    3
0    4
dtype: int64

Чтобы вернуть Series той же формы, что и исходная:

In [200]: s.where(s > 0)
Out[200]: 
4    NaN
3    1.0
2    2.0
1    3.0
0    4.0
dtype: float64

При выборе значений из DataFrame с помощью булева критерия исходная форма данных также сохраняется. Внутренне для реализации используется where. Приведённый ниже код эквивалентен df.where(df < 0).

In [201]: dates = pd.date_range('1/1/2000', periods=8)

In [202]: df = pd.DataFrame(np.random.randn(8, 4),
   .....:                   index=dates, columns=['A', 'B', 'C', 'D'])
   .....: 

In [203]: df[df < 0]
Out[203]: 
                   A         B         C         D
2000-01-01 -2.104139 -1.309525       NaN       NaN
2000-01-02 -0.352480       NaN -1.192319       NaN
2000-01-03 -0.864883       NaN -0.227870       NaN
2000-01-04       NaN -1.222082       NaN -1.233203
2000-01-05       NaN -0.605656 -1.169184       NaN
2000-01-06       NaN -0.948458       NaN -0.684718
2000-01-07 -2.670153 -0.114722       NaN -0.048048
2000-01-08       NaN       NaN -0.048788 -0.808838

Кроме того, where принимает необязательный аргумент other для замены значений, для которых условие ложно, в возвращаемой копии.

In [204]: df.where(df < 0, -df)
Out[204]: 
                   A         B         C         D
2000-01-01 -2.104139 -1.309525 -0.485855 -0.245166
2000-01-02 -0.352480 -0.390389 -1.192319 -1.655824
2000-01-03 -0.864883 -0.299674 -0.227870 -0.281059
2000-01-04 -0.846958 -1.222082 -0.600705 -1.233203
2000-01-05 -0.669692 -0.605656 -1.169184 -0.342416
2000-01-06 -0.868584 -0.948458 -2.297780 -0.684718
2000-01-07 -2.670153 -0.114722 -0.168904 -0.048048
2000-01-08 -0.801196 -1.392071 -0.048788 -0.808838

Возможно, вы захотите задать значения на основе некоторого булева критерия. Это можно сделать интуитивно понятным способом:

In [205]: s2 = s.copy()

In [206]: s2[s2 < 0] = 0

In [207]: s2
Out[207]: 
4    0
3    1
2    2
1    3
0    4
dtype: int64

In [208]: df2 = df.copy()

In [209]: df2[df2 < 0] = 0

In [210]: df2
Out[210]: 
                   A         B         C         D
2000-01-01  0.000000  0.000000  0.485855  0.245166
2000-01-02  0.000000  0.390389  0.000000  1.655824
2000-01-03  0.000000  0.299674  0.000000  0.281059
2000-01-04  0.846958  0.000000  0.600705  0.000000
2000-01-05  0.669692  0.000000  0.000000  0.342416
2000-01-06  0.868584  0.000000  2.297780  0.000000
2000-01-07  0.000000  0.000000  0.168904  0.000000
2000-01-08  0.801196  1.392071  0.000000  0.000000

where возвращает изменённую копию данных.

Примечание

Сигнатура DataFrame.where() отличается от сигнатуры numpy.where(). Приблизительно df1.where(m, df2) эквивалентно np.where(m, df1, df2).

In [211]: df.where(df < 0, -df) == np.where(df < 0, df, -df)
Out[211]: 
               A     B     C     D
2000-01-01  True  True  True  True
2000-01-02  True  True  True  True
2000-01-03  True  True  True  True
2000-01-04  True  True  True  True
2000-01-05  True  True  True  True
2000-01-06  True  True  True  True
2000-01-07  True  True  True  True
2000-01-08  True  True  True  True

Выравнивание

Кроме того, where выравнивает входное булево условие (ndarray или DataFrame), позволяя выполнять частичный выбор с присваиванием. Это аналогично частичному присваиванию с помощью .loc (но для содержимого, а не для меток осей).

In [212]: df2 = df.copy()

In [213]: df2[df2[1:4] > 0] = 3

In [214]: df2
Out[214]: 
                   A         B         C         D
2000-01-01 -2.104139 -1.309525  0.485855  0.245166
2000-01-02 -0.352480  3.000000 -1.192319  3.000000
2000-01-03 -0.864883  3.000000 -0.227870  3.000000
2000-01-04  3.000000 -1.222082  3.000000 -1.233203
2000-01-05  0.669692 -0.605656 -1.169184  0.342416
2000-01-06  0.868584 -0.948458  2.297780 -0.684718
2000-01-07 -2.670153 -0.114722  0.168904 -0.048048
2000-01-08  0.801196  1.392071 -0.048788 -0.808838

При выполнении where метод where также принимает параметры axis и level для выравнивания входных данных.

In [215]: df2 = df.copy()

In [216]: df2.where(df2 > 0, df2['A'], axis='index')
Out[216]: 
                   A         B         C         D
2000-01-01 -2.104139 -2.104139  0.485855  0.245166
2000-01-02 -0.352480  0.390389 -0.352480  1.655824
2000-01-03 -0.864883  0.299674 -0.864883  0.281059
2000-01-04  0.846958  0.846958  0.600705  0.846958
2000-01-05  0.669692  0.669692  0.669692  0.342416
2000-01-06  0.868584  0.868584  2.297780  0.868584
2000-01-07 -2.670153 -2.670153  0.168904 -2.670153
2000-01-08  0.801196  1.392071  0.801196  0.801196

Это эквивалентно следующему (но работает быстрее).

In [217]: df2 = df.copy()

In [218]: df.apply(lambda x, y: x.where(x > 0, y), y=df['A'])
Out[218]: 
                   A         B         C         D
2000-01-01 -2.104139 -2.104139  0.485855  0.245166
2000-01-02 -0.352480  0.390389 -0.352480  1.655824
2000-01-03 -0.864883  0.299674 -0.864883  0.281059
2000-01-04  0.846958  0.846958  0.600705  0.846958
2000-01-05  0.669692  0.669692  0.669692  0.342416
2000-01-06  0.868584  0.868584  2.297780  0.868584
2000-01-07 -2.670153 -2.670153  0.168904 -2.670153
2000-01-08  0.801196  1.392071  0.801196  0.801196

В качестве условия и аргументов other метод where может принимать вызываемый объект. Функция должна принимать один аргумент (вызывающий её объект Series или DataFrame) и возвращать допустимый результат для условия и аргумента other.

In [219]: df3 = pd.DataFrame({'A': [1, 2, 3],
   .....:                     'B': [4, 5, 6],
   .....:                     'C': [7, 8, 9]})
   .....: 

In [220]: df3.where(lambda x: x > 4, lambda x: x + 10)
Out[220]: 
    A   B  C
0  11  14  7
1  12   5  8
2  13   6  9

Маска

mask() выполняет обратную булеву операцию по отношению к where.

In [221]: s.mask(s >= 0)
Out[221]: 
4   NaN
3   NaN
2   NaN
1   NaN
0   NaN
dtype: float64

In [222]: df.mask(df >= 0)
Out[222]: 
                   A         B         C         D
2000-01-01 -2.104139 -1.309525       NaN       NaN
2000-01-02 -0.352480       NaN -1.192319       NaN
2000-01-03 -0.864883       NaN -0.227870       NaN
2000-01-04       NaN -1.222082       NaN -1.233203
2000-01-05       NaN -0.605656 -1.169184       NaN
2000-01-06       NaN -0.948458       NaN -0.684718
2000-01-07 -2.670153 -0.114722       NaN -0.048048
2000-01-08       NaN       NaN -0.048788 -0.808838

Условное присваивание с расширением с помощью numpy()

Альтернативой where() является использование numpy.where(). В сочетании с добавлением нового столбца этот метод позволяет расширить DataFrame, задавая значения по условию.

Предположим, в следующем DataFrame есть два варианта на выбор. Вы хотите задать для нового столбца color значение ‘green’, если во втором столбце находится ‘Z’. Это можно сделать следующим образом:

In [223]: df = pd.DataFrame({'col1': list('ABBC'), 'col2': list('ZZXY')})

In [224]: df['color'] = np.where(df['col2'] == 'Z', 'green', 'red')

In [225]: df
Out[225]: 
  col1 col2  color
0    A    Z  green
1    B    Z  green
2    B    X    red
3    C    Y    red

Если условий несколько, для этого можно использовать numpy.select(). Предположим, что трём условиям соответствуют три варианта цвета, а четвёртый цвет используется по умолчанию. Тогда можно сделать следующее:

In [226]: conditions = [
   .....:     (df['col2'] == 'Z') & (df['col1'] == 'A'),
   .....:     (df['col2'] == 'Z') & (df['col1'] == 'B'),
   .....:     (df['col1'] == 'B')
   .....: ]
   .....: 

In [227]: choices = ['yellow', 'blue', 'purple']

In [228]: df['color'] = np.select(conditions, choices, default='black')

In [229]: df
Out[229]: 
  col1 col2   color
0    A    Z  yellow
1    B    Z    blue
2    B    X  purple
3    C    Y   black

Метод query()

Объекты DataFrame имеют метод query(), который позволяет выполнять выборку с помощью выражения.

Можно получить значения фрейма, в котором значения столбца b находятся между значениями столбцов a и c. Например:

In [230]: n = 10

In [231]: df = pd.DataFrame(np.random.rand(n, 3), columns=list('abc'))

In [232]: df
Out[232]: 
          a         b         c
0  0.438921  0.118680  0.863670
1  0.138138  0.577363  0.686602
2  0.595307  0.564592  0.520630
3  0.913052  0.926075  0.616184
4  0.078718  0.854477  0.898725
5  0.076404  0.523211  0.591538
6  0.792342  0.216974  0.564056
7  0.397890  0.454131  0.915716
8  0.074315  0.437913  0.019794
9  0.559209  0.502065  0.026437

# pure python
In [233]: df[(df['a'] < df['b']) & (df['b'] < df['c'])]
Out[233]: 
          a         b         c
1  0.138138  0.577363  0.686602
4  0.078718  0.854477  0.898725
5  0.076404  0.523211  0.591538
7  0.397890  0.454131  0.915716

# query
In [234]: df.query('(a < b) & (b < c)')
Out[234]: 
          a         b         c
1  0.138138  0.577363  0.686602
4  0.078718  0.854477  0.898725
5  0.076404  0.523211  0.591538
7  0.397890  0.454131  0.915716

Сделайте то же самое, но используйте именованный индекс, если столбца с именем a нет.

In [235]: df = pd.DataFrame(np.random.randint(n / 2, size=(n, 2)), columns=list('bc'))

In [236]: df.index.name = 'a'

In [237]: df
Out[237]: 
   b  c
a      
0  0  4
1  0  1
2  3  4
3  4  3
4  1  4
5  0  3
6  0  1
7  3  4
8  2  3
9  1  1

In [238]: df.query('a < b and b < c')
Out[238]: 
   b  c
a      
2  3  4

Если вы не хотите или не можете задать имя индекса, используйте в выражении запроса имя index:

In [239]: df = pd.DataFrame(np.random.randint(n, size=(n, 2)), columns=list('bc'))

In [240]: df
Out[240]: 
   b  c
0  3  1
1  3  0
2  5  6
3  5  2
4  7  4
5  0  1
6  2  5
7  0  1
8  6  0
9  7  9

In [241]: df.query('index < b < c')
Out[241]: 
   b  c
2  5  6

Примечание

Если имя индекса совпадает с именем столбца, приоритет отдаётся имени столбца. Например:

In [242]: df = pd.DataFrame({'a': np.random.randint(5, size=5)})

In [243]: df.index.name = 'a'

In [244]: df.query('a > 2')  # uses the column 'a', not the index
Out[244]: 
   a
a   
1  3
3  3

Тем не менее, индекс можно использовать в выражении запроса, указав специальный идентификатор ‘index’:

In [245]: df.query('index > 2')
Out[245]: 
   a
a   
3  3
4  2

Если по какой-либо причине у вас есть столбец с именем index, индекс также можно указать как ilevel_0, но в этом случае стоит переименовать столбцы, чтобы избежать неоднозначности.

Синтаксис MultiIndex для query()

Уровни DataFrame также можно использовать вместе с MultiIndex, как если бы они были столбцами фрейма:

In [246]: n = 10

In [247]: colors = np.random.choice(['red', 'green'], size=n)

In [248]: foods = np.random.choice(['eggs', 'ham'], size=n)

In [249]: colors
Out[249]: 
array(['red', 'red', 'red', 'green', 'green', 'green', 'green', 'green',
       'green', 'green'], dtype='<U5')

In [250]: foods
Out[250]: 
array(['ham', 'ham', 'eggs', 'eggs', 'eggs', 'ham', 'ham', 'eggs', 'eggs',
       'eggs'], dtype='<U4')

In [251]: index = pd.MultiIndex.from_arrays([colors, foods], names=['color', 'food'])

In [252]: df = pd.DataFrame(np.random.randn(n, 2), index=index)

In [253]: df
Out[253]: 
                   0         1
color food                    
red   ham   0.194889 -0.381994
      ham   0.318587  2.089075
      eggs -0.728293 -0.090255
green eggs -0.748199  1.318931
      eggs -2.029766  0.792652
      ham   0.461007 -0.542749
      ham  -0.305384 -0.479195
      eggs  0.095031 -0.270099
      eggs -0.707140 -0.773882
      eggs  0.229453  0.304418

In [254]: df.query('color == "red"')
Out[254]: 
                   0         1
color food                    
red   ham   0.194889 -0.381994
      ham   0.318587  2.089075
      eggs -0.728293 -0.090255

Если уровни MultiIndex не имеют имён, на них можно ссылаться с помощью специальных имён:

In [255]: df.index.names = [None, None]

In [256]: df
Out[256]: 
                   0         1
red   ham   0.194889 -0.381994
      ham   0.318587  2.089075
      eggs -0.728293 -0.090255
green eggs -0.748199  1.318931
      eggs -2.029766  0.792652
      ham   0.461007 -0.542749
      ham  -0.305384 -0.479195
      eggs  0.095031 -0.270099
      eggs -0.707140 -0.773882
      eggs  0.229453  0.304418

In [257]: df.query('ilevel_0 == "red"')
Out[257]: 
                 0         1
red ham   0.194889 -0.381994
    ham   0.318587  2.089075
    eggs -0.728293 -0.090255

По соглашению используется ilevel_0, что означает «уровень индекса 0» для нулевого уровня index.

Примеры использования query()

Один из случаев применения query() — наличие набора объектов DataFrame с общим подмножеством имён столбцов (или уровней/имён индекса). Можно передать один и тот же запрос обоим фреймам, не указывая, к какому именно фрейму он относится.

In [258]: df = pd.DataFrame(np.random.rand(n, 3), columns=list('abc'))

In [259]: df
Out[259]: 
          a         b         c
0  0.224283  0.736107  0.139168
1  0.302827  0.657803  0.713897
2  0.611185  0.136624  0.984960
3  0.195246  0.123436  0.627712
4  0.618673  0.371660  0.047902
5  0.480088  0.062993  0.185760
6  0.568018  0.483467  0.445289
7  0.309040  0.274580  0.587101
8  0.258993  0.477769  0.370255
9  0.550459  0.840870  0.304611

In [260]: df2 = pd.DataFrame(np.random.rand(n + 2, 3), columns=df.columns)

In [261]: df2
Out[261]: 
           a         b         c
0   0.357579  0.229800  0.596001
1   0.309059  0.957923  0.965663
2   0.123102  0.336914  0.318616
3   0.526506  0.323321  0.860813
4   0.518736  0.486514  0.384724
5   0.190804  0.505723  0.614533
6   0.891939  0.623977  0.676639
7   0.480559  0.378528  0.460858
8   0.420223  0.136404  0.141295
9   0.732206  0.419540  0.604675
10  0.604466  0.848974  0.896165
11  0.589168  0.920046  0.732716

In [262]: expr = '0.0 <= a <= c <= 0.5'

In [263]: map(lambda frame: frame.query(expr), [df, df2])
Out[263]: <map at 0x7f946de71e40>

Сравнение синтаксиса query() в Python и pandas

Полный синтаксис в стиле NumPy:

In [264]: df = pd.DataFrame(np.random.randint(n, size=(n, 3)), columns=list('abc'))

In [265]: df
Out[265]: 
   a  b  c
0  7  8  9
1  1  0  7
2  2  7  2
3  6  2  2
4  2  6  3
5  3  8  2
6  1  7  2
7  5  1  5
8  9  8  0
9  1  5  0

In [266]: df.query('(a < b) & (b < c)')
Out[266]: 
   a  b  c
0  7  8  9

In [267]: df[(df['a'] < df['b']) & (df['b'] < df['c'])]
Out[267]: 
   a  b  c
0  7  8  9

Немного удобнее без скобок (операторы сравнения имеют более высокий приоритет, чем & и |):

In [268]: df.query('a < b & b < c')
Out[268]: 
   a  b  c
0  7  8  9

Использование слов вместо символов:

In [269]: df.query('a < b and b < c')
Out[269]: 
   a  b  c
0  7  8  9

Почти так же, как это можно было бы записать на бумаге:

In [270]: df.query('a < b < c')
Out[270]: 
   a  b  c
0  7  8  9

Операторы in и not in

query() также поддерживает специальное использование операторов сравнения Python in и not in, обеспечивая краткий синтаксис для вызова метода isin объекта Series или DataFrame.

# get all rows where columns "a" and "b" have overlapping values
In [271]: df = pd.DataFrame({'a': list('aabbccddeeff'), 'b': list('aaaabbbbcccc'),
   .....:                    'c': np.random.randint(5, size=12),
   .....:                    'd': np.random.randint(9, size=12)})
   .....: 

In [272]: df
Out[272]: 
    a  b  c  d
0   a  a  2  6
1   a  a  4  7
2   b  a  1  6
3   b  a  2  1
4   c  b  3  6
5   c  b  0  2
6   d  b  3  3
7   d  b  2  1
8   e  c  4  3
9   e  c  2  0
10  f  c  0  6
11  f  c  1  2

In [273]: df.query('a in b')
Out[273]: 
   a  b  c  d
0  a  a  2  6
1  a  a  4  7
2  b  a  1  6
3  b  a  2  1
4  c  b  3  6
5  c  b  0  2

# How you'd do it in pure Python
In [274]: df[df['a'].isin(df['b'])]
Out[274]: 
   a  b  c  d
0  a  a  2  6
1  a  a  4  7
2  b  a  1  6
3  b  a  2  1
4  c  b  3  6
5  c  b  0  2

In [275]: df.query('a not in b')
Out[275]: 
    a  b  c  d
6   d  b  3  3
7   d  b  2  1
8   e  c  4  3
9   e  c  2  0
10  f  c  0  6
11  f  c  1  2

# pure Python
In [276]: df[~df['a'].isin(df['b'])]
Out[276]: 
    a  b  c  d
6   d  b  3  3
7   d  b  2  1
8   e  c  4  3
9   e  c  2  0
10  f  c  0  6
11  f  c  1  2

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

# rows where cols a and b have overlapping values
# and col c's values are less than col d's
In [277]: df.query('a in b and c < d')
Out[277]: 
   a  b  c  d
0  a  a  2  6
1  a  a  4  7
2  b  a  1  6
4  c  b  3  6
5  c  b  0  2

# pure Python
In [278]: df[df['b'].isin(df['a']) & (df['c'] < df['d'])]
Out[278]: 
    a  b  c  d
0   a  a  2  6
1   a  a  4  7
2   b  a  1  6
4   c  b  3  6
5   c  b  0  2
10  f  c  0  6
11  f  c  1  2

Примечание

Обратите внимание, что in и not in вычисляются в Python, поскольку в numexpr нет аналога этой операции. Однако в обычном Python вычисляется только само выражение in/not in. Например, в выражении

df.query('a in b + c + d')

(b + c + d) вычисляется с помощью numexpr, а затем операция in вычисляется в обычном Python. В общем случае любые операции, которые можно вычислить с помощью numexpr, будут вычислены именно так.

Особое использование оператора == с объектами list

Сравнение list значений со столбцом с помощью ==/!= работает аналогично in/not in.

In [279]: df.query('b == ["a", "b", "c"]')
Out[279]: 
    a  b  c  d
0   a  a  2  6
1   a  a  4  7
2   b  a  1  6
3   b  a  2  1
4   c  b  3  6
5   c  b  0  2
6   d  b  3  3
7   d  b  2  1
8   e  c  4  3
9   e  c  2  0
10  f  c  0  6
11  f  c  1  2

# pure Python
In [280]: df[df['b'].isin(["a", "b", "c"])]
Out[280]: 
    a  b  c  d
0   a  a  2  6
1   a  a  4  7
2   b  a  1  6
3   b  a  2  1
4   c  b  3  6
5   c  b  0  2
6   d  b  3  3
7   d  b  2  1
8   e  c  4  3
9   e  c  2  0
10  f  c  0  6
11  f  c  1  2

In [281]: df.query('c == [1, 2]')
Out[281]: 
    a  b  c  d
0   a  a  2  6
2   b  a  1  6
3   b  a  2  1
7   d  b  2  1
9   e  c  2  0
11  f  c  1  2

In [282]: df.query('c != [1, 2]')
Out[282]: 
    a  b  c  d
1   a  a  4  7
4   c  b  3  6
5   c  b  0  2
6   d  b  3  3
8   e  c  4  3
10  f  c  0  6

# using in/not in
In [283]: df.query('[1, 2] in c')
Out[283]: 
    a  b  c  d
0   a  a  2  6
2   b  a  1  6
3   b  a  2  1
7   d  b  2  1
9   e  c  2  0
11  f  c  1  2

In [284]: df.query('[1, 2] not in c')
Out[284]: 
    a  b  c  d
1   a  a  4  7
4   c  b  3  6
5   c  b  0  2
6   d  b  3  3
8   e  c  4  3
10  f  c  0  6

# pure Python
In [285]: df[df['c'].isin([1, 2])]
Out[285]: 
    a  b  c  d
0   a  a  2  6
2   b  a  1  6
3   b  a  2  1
7   d  b  2  1
9   e  c  2  0
11  f  c  1  2

Булевы операторы

Булевы выражения можно инвертировать с помощью слова not или оператора ~.

In [286]: df = pd.DataFrame(np.random.rand(n, 3), columns=list('abc'))

In [287]: df['bools'] = np.random.rand(len(df)) > 0.5

In [288]: df.query('~bools')
Out[288]: 
          a         b         c  bools
2  0.697753  0.212799  0.329209  False
7  0.275396  0.691034  0.826619  False
8  0.190649  0.558748  0.262467  False

In [289]: df.query('not bools')
Out[289]: 
          a         b         c  bools
2  0.697753  0.212799  0.329209  False
7  0.275396  0.691034  0.826619  False
8  0.190649  0.558748  0.262467  False

In [290]: df.query('not bools') == df[~df['bools']]
Out[290]: 
      a     b     c  bools
2  True  True  True   True
7  True  True  True   True
8  True  True  True   True

Разумеется, выражения могут быть сколь угодно сложными:

# short query syntax
In [291]: shorter = df.query('a < b < c and (not bools) or bools > 2')

# equivalent in pure Python
In [292]: longer = df[(df['a'] < df['b'])
   .....:             & (df['b'] < df['c'])
   .....:             & (~df['bools'])
   .....:             | (df['bools'] > 2)]
   .....: 

In [293]: shorter
Out[293]: 
          a         b         c  bools
7  0.275396  0.691034  0.826619  False

In [294]: longer
Out[294]: 
          a         b         c  bools
7  0.275396  0.691034  0.826619  False

In [295]: shorter == longer
Out[295]: 
      a     b     c  bools
7  True  True  True   True

Производительность query()

DataFrame.query() с использованием numexpr работает немного быстрее Python на больших фреймах.

../_images/query-perf.png

Преимущество в производительности при использовании движка numexpr с DataFrame.query() заметно только в том случае, если фрейм содержит более 100 000 строк.

Этот график построен на основе DataFrame с тремя столбцами, каждый из которых содержит значения с плавающей точкой, созданные с помощью numpy.random.randn().

In [296]: df = pd.DataFrame(np.random.randn(8, 4),
   .....:                   index=dates, columns=['A', 'B', 'C', 'D'])
   .....: 

In [297]: df2 = df.copy()

Дублирующиеся данные

Чтобы найти и удалить дублирующиеся строки в DataFrame, можно воспользоваться двумя методами: duplicated и drop_duplicates. Каждый из них принимает в качестве аргумента столбцы, используемые для определения дублирующихся строк.

  • duplicated возвращает булев вектор длиной, равной числу строк, который показывает, является ли строка дубликатом.

  • drop_duplicates удаляет дублирующиеся строки.

По умолчанию первая встреченная строка из группы дубликатов считается уникальной, но каждый метод имеет параметр keep, позволяющий указать, какие строки следует сохранить.

  • keep='first' (по умолчанию): пометить / удалить дубликаты, кроме первого вхождения.

  • keep='last': пометить / удалить дубликаты, кроме последнего вхождения.

  • keep=False: пометить / удалить все дубликаты.

In [298]: df2 = pd.DataFrame({'a': ['one', 'one', 'two', 'two', 'two', 'three', 'four'],
   .....:                     'b': ['x', 'y', 'x', 'y', 'x', 'x', 'x'],
   .....:                     'c': np.random.randn(7)})
   .....: 

In [299]: df2
Out[299]: 
       a  b         c
0    one  x -1.067137
1    one  y  0.309500
2    two  x -0.211056
3    two  y -1.842023
4    two  x -0.390820
5  three  x -1.964475
6   four  x  1.298329

In [300]: df2.duplicated('a')
Out[300]: 
0    False
1     True
2    False
3     True
4     True
5    False
6    False
dtype: bool

In [301]: df2.duplicated('a', keep='last')
Out[301]: 
0     True
1    False
2     True
3     True
4    False
5    False
6    False
dtype: bool

In [302]: df2.duplicated('a', keep=False)
Out[302]: 
0     True
1     True
2     True
3     True
4     True
5    False
6    False
dtype: bool

In [303]: df2.drop_duplicates('a')
Out[303]: 
       a  b         c
0    one  x -1.067137
2    two  x -0.211056
5  three  x -1.964475
6   four  x  1.298329

In [304]: df2.drop_duplicates('a', keep='last')
Out[304]: 
       a  b         c
1    one  y  0.309500
4    two  x -0.390820
5  three  x -1.964475
6   four  x  1.298329

In [305]: df2.drop_duplicates('a', keep=False)
Out[305]: 
       a  b         c
5  three  x -1.964475
6   four  x  1.298329

Для поиска дубликатов также можно передать список столбцов.

In [306]: df2.duplicated(['a', 'b'])
Out[306]: 
0    False
1    False
2    False
3    False
4     True
5    False
6    False
dtype: bool

In [307]: df2.drop_duplicates(['a', 'b'])
Out[307]: 
       a  b         c
0    one  x -1.067137
1    one  y  0.309500
2    two  x -0.211056
3    two  y -1.842023
5  three  x -1.964475
6   four  x  1.298329

Чтобы удалить дубликаты по значению индекса, используйте Index.duplicated, а затем выполните срез. Для параметра keep доступны те же наборы параметров.

In [308]: df3 = pd.DataFrame({'a': np.arange(6),
   .....:                     'b': np.random.randn(6)},
   .....:                    index=['a', 'a', 'b', 'c', 'b', 'a'])
   .....: 

In [309]: df3
Out[309]: 
   a         b
a  0  1.440455
a  1  2.456086
b  2  1.038402
c  3 -0.894409
b  4  0.683536
a  5  3.082764

In [310]: df3.index.duplicated()
Out[310]: array([False,  True, False, False,  True,  True])

In [311]: df3[~df3.index.duplicated()]
Out[311]: 
   a         b
a  0  1.440455
b  2  1.038402
c  3 -0.894409

In [312]: df3[~df3.index.duplicated(keep='last')]
Out[312]: 
   a         b
c  3 -0.894409
b  4  0.683536
a  5  3.082764

In [313]: df3[~df3.index.duplicated(keep=False)]
Out[313]: 
   a         b
c  3 -0.894409

Метод get(), похожий на метод словаря

У объектов Series и DataFrame есть метод get, который может возвращать значение по умолчанию.

In [314]: s = pd.Series([1, 2, 3], index=['a', 'b', 'c'])

In [315]: s.get('a')  # equivalent to s['a']
Out[315]: 1

In [316]: s.get('x', default=-1)
Out[316]: -1

Поиск значений по меткам индекса/столбцов

Иногда требуется извлечь набор значений по последовательностям меток строк и столбцов. Это можно сделать с помощью pandas.factorize и индексации NumPy. Например:

In [317]: df = pd.DataFrame({'col': ["A", "A", "B", "B"],
   .....:                    'A': [80, 23, np.nan, 22],
   .....:                    'B': [80, 55, 76, 67]})
   .....: 

In [318]: df
Out[318]: 
  col     A   B
0   A  80.0  80
1   A  23.0  55
2   B   NaN  76
3   B  22.0  67

In [319]: idx, cols = pd.factorize(df['col'])

In [320]: df.reindex(cols, axis=1).to_numpy()[np.arange(len(df)), idx]
Out[320]: array([80., 23., 76., 67.])

Ранее это можно было сделать с помощью специального метода DataFrame.lookup, который был объявлен устаревшим в версии 1.2.0 и удалён в версии 2.0.0.

Объекты Index

Класс pandas Index и его подклассы можно рассматривать как реализацию упорядоченного мультимножества. Дубликаты допускаются.

Index также предоставляет инфраструктуру, необходимую для поиска, выравнивания данных и повторной индексации. Проще всего создать Index напрямую, передав list или другую последовательность в Index:

In [321]: index = pd.Index(['e', 'd', 'a', 'b'])

In [322]: index
Out[322]: Index(['e', 'd', 'a', 'b'], dtype='object')

In [323]: 'd' in index
Out[323]: True

или используя числа:

In [324]: index = pd.Index([1, 5, 12])

In [325]: index
Out[325]: Index([1, 5, 12], dtype='int64')

In [326]: 5 in index
Out[326]: True

Если dtype не указан, Index пытается определить его по данным. При создании экземпляра Index также можно указать dtype явно:

In [327]: index = pd.Index(['e', 'd', 'a', 'b'], dtype="string")

In [328]: index
Out[328]: Index(['e', 'd', 'a', 'b'], dtype='string')

In [329]: index = pd.Index([1, 5, 12], dtype="int8")

In [330]: index
Out[330]: Index([1, 5, 12], dtype='int8')

In [331]: index = pd.Index([1, 5, 12], dtype="float32")

In [332]: index
Out[332]: Index([1.0, 5.0, 12.0], dtype='float32')

В индекс также можно передать name:

In [333]: index = pd.Index(['e', 'd', 'a', 'b'], name='something')

In [334]: index.name
Out[334]: 'something'

Если имя задано, оно отображается в консоли:

In [335]: index = pd.Index(list(range(5)), name='rows')

In [336]: columns = pd.Index(['A', 'B', 'C'], name='cols')

In [337]: df = pd.DataFrame(np.random.randn(5, 3), index=index, columns=columns)

In [338]: df
Out[338]: 
cols         A         B         C
rows                              
0     1.295989 -1.051694  1.340429
1    -2.366110  0.428241  0.387275
2     0.433306  0.929548  0.278094
3     2.154730 -0.315628  0.264223
4     1.126818  1.132290 -0.353310

In [339]: df['A']
Out[339]: 
rows
0    1.295989
1   -2.366110
2    0.433306
3    2.154730
4    1.126818
Name: A, dtype: float64

Задание метаданных

Индексы «в основном неизменяемы», но их атрибут name можно задавать и изменять. Для непосредственного задания этих атрибутов можно использовать rename, set_names; по умолчанию они возвращают копию.

Информацию об использовании MultiIndex см. в разделе Расширенная индексация.

In [340]: ind = pd.Index([1, 2, 3])

In [341]: ind.rename("apple")
Out[341]: Index([1, 2, 3], dtype='int64', name='apple')

In [342]: ind
Out[342]: Index([1, 2, 3], dtype='int64')

In [343]: ind = ind.set_names(["apple"])

In [344]: ind.name = "bob"

In [345]: ind
Out[345]: Index([1, 2, 3], dtype='int64', name='bob')

set_names, set_levels и set_codes также принимают необязательный аргумент level.

In [346]: index = pd.MultiIndex.from_product([range(3), ['one', 'two']], names=['first', 'second'])

In [347]: index
Out[347]: 
MultiIndex([(0, 'one'),
            (0, 'two'),
            (1, 'one'),
            (1, 'two'),
            (2, 'one'),
            (2, 'two')],
           names=['first', 'second'])

In [348]: index.levels[1]
Out[348]: Index(['one', 'two'], dtype='object', name='second')

In [349]: index.set_levels(["a", "b"], level=1)
Out[349]: 
MultiIndex([(0, 'a'),
            (0, 'b'),
            (1, 'a'),
            (1, 'b'),
            (2, 'a'),
            (2, 'b')],
           names=['first', 'second'])

Операции над множествами для объектов Index

Две основные операции — union и intersection. Разность вычисляется с помощью метода .difference().

In [350]: a = pd.Index(['c', 'b', 'a'])

In [351]: b = pd.Index(['c', 'e', 'd'])

In [352]: a.difference(b)
Out[352]: Index(['a', 'b'], dtype='object')

Также доступна операция symmetric_difference, возвращающая элементы, которые встречаются в idx1 или idx2, но не в обоих индексах одновременно. Она эквивалентна индексу, созданному с помощью idx1.difference(idx2).union(idx2.difference(idx1)), с удалёнными дубликатами.

In [353]: idx1 = pd.Index([1, 2, 3, 4])

In [354]: idx2 = pd.Index([2, 3, 4, 5])

In [355]: idx1.symmetric_difference(idx2)
Out[355]: Index([1, 5], dtype='int64')

Примечание

Результирующий индекс после операции над множествами будет отсортирован по возрастанию.

При выполнении Index.union() для индексов с разными типами данных их необходимо привести к общему типу. Обычно, хотя и не всегда, это тип object. Исключение составляет объединение целочисленных данных и данных с плавающей точкой. В этом случае целочисленные значения преобразуются в числа с плавающей точкой.

In [356]: idx1 = pd.Index([0, 1, 2])

In [357]: idx2 = pd.Index([0.5, 1.5])

In [358]: idx1.union(idx2)
Out[358]: Index([0.0, 0.5, 1.0, 1.5, 2.0], dtype='float64')

Отсутствующие значения

Важно

Хотя Index может содержать отсутствующие значения (NaN), этого следует избегать, если вы не хотите столкнуться с неожиданными результатами. Например, некоторые операции неявно исключают отсутствующие значения.

Index.fillna заполняет отсутствующие значения указанным скалярным значением.

In [359]: idx1 = pd.Index([1, np.nan, 3, 4])

In [360]: idx1
Out[360]: Index([1.0, nan, 3.0, 4.0], dtype='float64')

In [361]: idx1.fillna(2)
Out[361]: Index([1.0, 2.0, 3.0, 4.0], dtype='float64')

In [362]: idx2 = pd.DatetimeIndex([pd.Timestamp('2011-01-01'),
   .....:                          pd.NaT,
   .....:                          pd.Timestamp('2011-01-03')])
   .....: 

In [363]: idx2
Out[363]: DatetimeIndex(['2011-01-01', 'NaT', '2011-01-03'], dtype='datetime64[ns]', freq=None)

In [364]: idx2.fillna(pd.Timestamp('2011-01-02'))
Out[364]: DatetimeIndex(['2011-01-01', '2011-01-02', '2011-01-03'], dtype='datetime64[ns]', freq=None)

Задание / сброс индекса

Иногда набор данных загружается или создаётся в DataFrame, и индекс требуется добавить уже после этого. Для этого есть несколько способов.

Задание индекса

У DataFrame есть метод set_index(), который принимает имя столбца (для обычного Index) или список имён столбцов (для MultiIndex). Чтобы создать новый DataFrame с переиндексированными данными:

In [365]: data = pd.DataFrame({'a': ['bar', 'bar', 'foo', 'foo'],
   .....:                      'b': ['one', 'two', 'one', 'two'],
   .....:                      'c': ['z', 'y', 'x', 'w'],
   .....:                      'd': [1., 2., 3, 4]})
   .....: 

In [366]: data
Out[366]: 
     a    b  c    d
0  bar  one  z  1.0
1  bar  two  y  2.0
2  foo  one  x  3.0
3  foo  two  w  4.0

In [367]: indexed1 = data.set_index('c')

In [368]: indexed1
Out[368]: 
     a    b    d
c               
z  bar  one  1.0
y  bar  two  2.0
x  foo  one  3.0
w  foo  two  4.0

In [369]: indexed2 = data.set_index(['a', 'b'])

In [370]: indexed2
Out[370]: 
         c    d
a   b          
bar one  z  1.0
    two  y  2.0
foo one  x  3.0
    two  w  4.0

Ключевой аргумент append позволяет сохранить существующий индекс и добавить указанные столбцы в MultiIndex:

In [371]: frame = data.set_index('c', drop=False)

In [372]: frame = frame.set_index(['a', 'b'], append=True)

In [373]: frame
Out[373]: 
           c    d
c a   b          
z bar one  z  1.0
y bar two  y  2.0
x foo one  x  3.0
w foo two  w  4.0

Другие параметры метода set_index позволяют не удалять столбцы индекса.

In [374]: data.set_index('c', drop=False)
Out[374]: 
     a    b  c    d
c                  
z  bar  one  z  1.0
y  bar  two  y  2.0
x  foo  one  x  3.0
w  foo  two  w  4.0

Сброс индекса

Для удобства в DataFrame добавлена функция reset_index(), которая переносит значения индекса в столбцы DataFrame и задаёт простой целочисленный индекс. Это обратная операция по отношению к set_index().

In [375]: data
Out[375]: 
     a    b  c    d
0  bar  one  z  1.0
1  bar  two  y  2.0
2  foo  one  x  3.0
3  foo  two  w  4.0

In [376]: data.reset_index()
Out[376]: 
   index    a    b  c    d
0      0  bar  one  z  1.0
1      1  bar  two  y  2.0
2      2  foo  one  x  3.0
3      3  foo  two  w  4.0

Результат больше похож на таблицу SQL или массив записей. Имена столбцов, полученных из индекса, берутся из атрибута names.

С помощью ключевого аргумента level можно удалить только часть индекса:

In [377]: frame
Out[377]: 
           c    d
c a   b          
z bar one  z  1.0
y bar two  y  2.0
x foo one  x  3.0
w foo two  w  4.0

In [378]: frame.reset_index(level=1)
Out[378]: 
         a  c    d
c b               
z one  bar  z  1.0
y two  bar  y  2.0
x one  foo  x  3.0
w two  foo  w  4.0

Метод reset_index принимает необязательный параметр drop, который при значении True просто отбрасывает индекс, а не помещает его значения в столбцы DataFrame.

Добавление временного индекса

Пользовательский индекс можно присвоить атрибуту index:

In [379]: df_idx = pd.DataFrame(range(4))

In [380]: df_idx.index = pd.Index([10, 20, 30, 40], name="a")

In [381]: df_idx
Out[381]: 
    0
a    
10  0
20  1
30  2
40  3

Возврат представления или копии

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

Copy-on-Write станет новым поведением по умолчанию в pandas 3.0. Это означает, что цепочечная индексация больше никогда не будет работать. Следовательно, SettingWithCopyWarning больше не понадобится. Дополнительную информацию см. в этом разделе. Рекомендуем включить Copy-on-Write, чтобы воспользоваться улучшениями с помощью

` pd.options.mode.copy_on_write = True `

даже до выхода pandas 3.0.

При установке значений в объекте pandas необходимо избегать так называемой chained indexing. Вот пример.

In [382]: dfmi = pd.DataFrame([list('abcd'),
   .....:                      list('efgh'),
   .....:                      list('ijkl'),
   .....:                      list('mnop')],
   .....:                     columns=pd.MultiIndex.from_product([['one', 'two'],
   .....:                                                         ['first', 'second']]))
   .....: 

In [383]: dfmi
Out[383]: 
    one          two       
  first second first second
0     a      b     c      d
1     e      f     g      h
2     i      j     k      l
3     m      n     o      p

Сравните эти два способа доступа:

In [384]: dfmi['one']['second']
Out[384]: 
0    b
1    f
2    j
3    n
Name: second, dtype: object
In [385]: dfmi.loc[:, ('one', 'second')]
Out[385]: 
0    b
1    f
2    j
3    n
Name: (one, second), dtype: object

Оба способа дают одинаковый результат, так какой же использовать? Полезно понять порядок выполнения операций в этих примерах и почему способ 2 (.loc) значительно предпочтительнее способа 1 (цепочечной []).

dfmi['one'] выбирает первый уровень столбцов и возвращает DataFrame с одним уровнем индекса. Затем другая операция Python dfmi_with_one['second'] выбирает серию с индексом 'second'. Это обозначено переменной dfmi_with_one, потому что pandas рассматривает эти операции как отдельные действия, например, отдельные вызовы __getitem__, поэтому должен выполнять их последовательно, одну за другой.

В отличие от этого, df.loc[:,('one','second')] передаёт вложенный кортеж из (slice(None),('one','second')) в единственный вызов __getitem__. Это позволяет pandas обработать его как единое целое. Кроме того, такой порядок операций может быть значительно быстрее и позволяет при необходимости индексировать обе оси.

Почему присваивание не работает при цепочечной индексации?

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

Copy-on-Write станет новым поведением по умолчанию в pandas 3.0. Это означает, что цепочечная индексация больше никогда не будет работать. Следовательно, SettingWithCopyWarning больше не понадобится. Дополнительную информацию см. в этом разделе. Рекомендуем включить Copy-on-Write, чтобы воспользоваться улучшениями с помощью

` pd.options.mode.copy_on_write = True `

даже до выхода pandas 3.0.

Проблема, описанная в предыдущем разделе, связана только с производительностью. Но что насчёт предупреждения SettingWithCopy? Мы же обычно не выдаём предупреждения о действиях, которые могут стоить нескольких лишних миллисекунд!

Однако присваивание результату цепочечной индексации даёт принципиально непредсказуемые результаты. Чтобы это понять, рассмотрим, как интерпретатор Python выполняет следующий код:

dfmi.loc[:, ('one', 'second')] = value
# becomes
dfmi.loc.__setitem__((slice(None), ('one', 'second')), value)

Но этот код обрабатывается иначе:

dfmi['one']['second'] = value
# becomes
dfmi.__getitem__('one').__setitem__('second', value)

Видите там __getitem__? За исключением простых случаев, очень трудно предсказать, вернёт ли он представление или копию (это зависит от расположения массива в памяти, которое pandas никак не гарантирует), а значит, будет ли __setitem__ изменять dfmi или временный объект, который сразу после этого будет удалён. Именно об этом и предупреждает SettingWithCopy!

Примечание

Возможно, вы задаётесь вопросом, нужно ли беспокоиться о свойстве loc в первом примере. Однако гарантируется, что dfmi.loc — это сама dfmi с изменённым поведением индексации, поэтому dfmi.loc.__getitem__ / dfmi.loc.__setitem__ работают непосредственно с dfmi. Конечно, dfmi.loc.__getitem__(idx) может быть представлением или копией dfmi.

Иногда предупреждение SettingWithCopy появляется даже тогда, когда явной цепочечной индексации нет. Именно такие ошибки призвано обнаруживать предупреждение SettingWithCopy! Вероятно, pandas пытается предупредить вас о том, что вы сделали следующее:

def do_something(df):
    foo = df[['bar', 'baz']]  # Is foo a view? A copy? Nobody knows!
    # ... many lines here ...
    # We don't know whether this will modify df or not!
    foo['quux'] = value
    return foo

Вот это да!

Порядок вычисления имеет значение

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

Copy-on-Write станет новым поведением по умолчанию в pandas 3.0. Это означает, что цепочечная индексация больше никогда не будет работать. Следовательно, SettingWithCopyWarning больше не понадобится. Дополнительную информацию см. в этом разделе. Рекомендуем включить Copy-on-Write, чтобы воспользоваться улучшениями с помощью

` pd.options.mode.copy_on_write = True `

даже до выхода pandas 3.0.

При цепочечной индексации порядок и тип операции индексации отчасти определяют, будет ли результат срезом исходного объекта или копией среза.

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

Если вы хотите, чтобы pandas более или менее строго относился к присваиванию в выражении с цепочечной индексацией, можно задать параметр mode.chained_assignment одним из следующих значений:

  • 'warn' — значение по умолчанию, означает, что будет выведено SettingWithCopyWarning.

  • 'raise' означает, что pandas вызовет SettingWithCopyError, которую вам придётся обработать.

  • None полностью отключает предупреждения.

In [386]: dfb = pd.DataFrame({'a': ['one', 'one', 'two',
   .....:                           'three', 'two', 'one', 'six'],
   .....:                     'c': np.arange(7)})
   .....: 

# This will show the SettingWithCopyWarning
# but the frame values will be set
In [387]: dfb['c'][dfb['a'].str.startswith('o')] = 42

Однако это выполняется над копией и работать не будет.

In [388]: with pd.option_context('mode.chained_assignment','warn'):
   .....:     dfb[dfb['a'].str.startswith('o')]['c'] = 42
   .....: 

Цепочечное присваивание может возникнуть и при установке значений в фрейме со смешанными типами данных.

Примечание

Эти правила установки значений применяются ко всем .loc/.iloc.

Ниже приведён рекомендуемый способ доступа: используйте .loc для нескольких элементов (с помощью mask) и фиксированный индекс для одного элемента:

In [389]: dfc = pd.DataFrame({'a': ['one', 'one', 'two',
   .....:                           'three', 'two', 'one', 'six'],
   .....:                     'c': np.arange(7)})
   .....: 

In [390]: dfd = dfc.copy()

# Setting multiple items using a mask
In [391]: mask = dfd['a'].str.startswith('o')

In [392]: dfd.loc[mask, 'c'] = 42

In [393]: dfd
Out[393]: 
       a   c
0    one  42
1    one  42
2    two   2
3  three   3
4    two   4
5    one  42
6    six   6

# Setting a single item
In [394]: dfd = dfc.copy()

In [395]: dfd.loc[2, 'a'] = 11

In [396]: dfd
Out[396]: 
       a  c
0    one  0
1    one  1
2     11  2
3  three  3
4    two  4
5    one  5
6    six  6

Следующий вариант иногда может работать, но это не гарантируется, поэтому его следует избегать:

In [397]: dfd = dfc.copy()

In [398]: dfd['a'][2] = 111

In [399]: dfd
Out[399]: 
       a  c
0    one  0
1    one  1
2    111  2
3  three  3
4    two  4
5    one  5
6    six  6

Наконец, следующий пример совсем не будет работать, поэтому его следует избегать:

In [400]: with pd.option_context('mode.chained_assignment','raise'):
   .....:     dfd.loc[0]['a'] = 1111
   .....: 
---------------------------------------------------------------------------
SettingWithCopyError                      Traceback (most recent call last)
<ipython-input-400-32ce785aaa5b> in ?()
      1 with pd.option_context('mode.chained_assignment','raise'):
----> 2     dfd.loc[0]['a'] = 1111

~/work/pandas/pandas/pandas/core/series.py in ?(self, key, value)
   1293                 )
   1294 
   1295         check_dict_or_set_indexers(key)
   1296         key = com.apply_if_callable(key, self)
-> 1297         cacher_needs_updating = self._check_is_chained_assignment_possible()
   1298 
   1299         if key is Ellipsis:
   1300             key = slice(None)

~/work/pandas/pandas/pandas/core/series.py in ?(self)
   1498             ref = self._get_cacher()
   1499             if ref is not None and ref._is_mixed_type:
   1500                 self._check_setitem_copy(t="referent", force=True)
   1501             return True
-> 1502         return super()._check_is_chained_assignment_possible()

~/work/pandas/pandas/pandas/core/generic.py in ?(self)
   4414         single-dtype meaning that the cacher should be updated following
   4415         setting.
   4416         """
   4417         if self._is_copy:
-> 4418             self._check_setitem_copy(t="referent")
   4419         return False

~/work/pandas/pandas/pandas/core/generic.py in ?(self, t, force)
   4488                 "indexing.html#returning-a-view-versus-a-copy"
   4489             )
   4490 
   4491         if value == "raise":
-> 4492             raise SettingWithCopyError(t)
   4493         if value == "warn":
   4494             warnings.warn(t, SettingWithCopyWarning, stacklevel=find_stack_level())

SettingWithCopyError: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

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

Предупреждения и исключения, связанные с цепочечным присваиванием, призваны сообщить пользователю о возможно некорректном присваивании. Возможны ложные срабатывания — ситуации, когда о цепочечном присваивании сообщается ошибочно.

© 2008–2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
© 2011–2025, Open source contributors
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/2.3.0/user_guide/indexing.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API