Spec-Zone.ru › pandas 2

Дублирующиеся метки

Объекты Index не обязаны быть уникальными; могут встречаться дублирующиеся метки строк или столбцов. Поначалу это может немного сбивать с толку. Если вы знакомы с SQL, то знаете, что метки строк похожи на первичный ключ таблицы, а в таблице SQL дубликаты совершенно нежелательны. Но одна из задач pandas — очищать неструктурированные данные из реального мира, прежде чем они попадут в последующую систему. А в данных из реального мира встречаются дубликаты, даже в полях, которые должны быть уникальными.

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

In [1]: import pandas as pd

In [2]: import numpy as np

Последствия дублирующихся меток

Некоторые методы pandas (например, Series.reindex()) просто не работают при наличии дубликатов. Результат нельзя определить, поэтому pandas вызывает исключение.

In [3]: s1 = pd.Series([0, 1, 2], index=["a", "b", "b"])

In [4]: s1.reindex(["a", "b", "c"])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 1
----> 1 s1.reindex(["a", "b", "c"])

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

Другие методы, например индексирование, могут приводить к неожиданным результатам. Обычно индексирование скалярным значением уменьшает размерность. Срез DataFrame по скалярному значению возвращает Series. Срез Series по скалярному значению возвращает скаляр. Но при наличии дубликатов это не так.

In [5]: df1 = pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "A", "B"])

In [6]: df1
Out[6]: 
   A  A  B
0  0  1  2
1  3  4  5

В столбцах есть дубликаты. Если выполнить срез 'B', мы получим Series

In [7]: df1["B"]  # a series
Out[7]: 
0    2
1    5
Name: B, dtype: int64

Но срез 'A' возвращает DataFrame

In [8]: df1["A"]  # a DataFrame
Out[8]: 
   A  A
0  0  1
1  3  4

Это относится и к меткам строк

In [9]: df2 = pd.DataFrame({"A": [0, 1, 2]}, index=["a", "a", "b"])

In [10]: df2
Out[10]: 
   A
a  0
a  1
b  2

In [11]: df2.loc["b", "A"]  # a scalar
Out[11]: 2

In [12]: df2.loc["a", "A"]  # a Series
Out[12]: 
a    0
a    1
Name: A, dtype: int64

Обнаружение дублирующихся меток

Уникальность Index (хранящего метки строк или столбцов) можно проверить с помощью Index.is_unique:

In [13]: df2
Out[13]: 
   A
a  0
a  1
b  2

In [14]: df2.index.is_unique
Out[14]: False

In [15]: df2.columns.is_unique
Out[15]: True

Примечание

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

Index.duplicated() возвращает логический ndarray, указывающий, повторяется ли метка.

In [16]: df2.index.duplicated()
Out[16]: array([False,  True, False])

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

In [17]: df2.loc[~df2.index.duplicated(), :]
Out[17]: 
   A
a  0
b  2

Если для обработки дублирующихся меток требуется дополнительная логика, а не просто удаление повторов, часто используют groupby() для индекса. Например, устраним дубликаты, вычислив среднее значение всех строк с одинаковой меткой.

In [18]: df2.groupby(level=0).mean()
Out[18]: 
     A
a  0.5
b  2.0

Запрет дублирующихся меток

Добавлено в версии 1.2.0.

Как отмечалось выше, обработка дубликатов — важная возможность при чтении исходных данных. При этом в процессе обработки данных может потребоваться избегать появления дубликатов (например, в результате вызова методов вроде pandas.concat(), rename() и т. д.). И Series, и DataFrame могут запретить дублирующиеся метки с помощью вызова .set_flags(allows_duplicate_labels=False). (По умолчанию они разрешены.) Если есть дублирующиеся метки, будет вызвано исключение.

In [19]: pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)
---------------------------------------------------------------------------
DuplicateLabelError                       Traceback (most recent call last)
Cell In[19], line 1
----> 1 pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)

File ~/work/pandas/pandas/pandas/core/generic.py:508, in NDFrame.set_flags(self, copy, allows_duplicate_labels)
    506 df = self.copy(deep=copy and not using_copy_on_write())
    507 if allows_duplicate_labels is not None:
--> 508     df.flags["allows_duplicate_labels"] = allows_duplicate_labels
    509 return df

File ~/work/pandas/pandas/pandas/core/flags.py:109, in Flags.__setitem__(self, key, value)
    107 if key not in self._keys:
    108     raise ValueError(f"Unknown flag {key}. Must be one of {self._keys}")
--> 109 setattr(self, key, value)

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
     94 if not value:
     95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
     98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:716, in Index._maybe_check_unique(self)
    713 duplicates = self._format_duplicate_message()
    714 msg += f"\n{duplicates}"
--> 716 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
b        [1, 2]

Для DataFrame это относится и к меткам строк, и к меткам столбцов

In [20]: pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "B", "C"],).set_flags(
   ....:     allows_duplicate_labels=False
   ....: )
   ....: 
Out[20]: 
   A  B  C
0  0  1  2
1  3  4  5

Этот атрибут можно проверить или задать с помощью allows_duplicate_labels; он указывает, допускаются ли дублирующиеся метки у этого объекта.

In [21]: df = pd.DataFrame({"A": [0, 1, 2, 3]}, index=["x", "y", "X", "Y"]).set_flags(
   ....:     allows_duplicate_labels=False
   ....: )
   ....: 

In [22]: df
Out[22]: 
   A
x  0
y  1
X  2
Y  3

In [23]: df.flags.allows_duplicate_labels
Out[23]: False

DataFrame.set_flags() можно использовать для возврата нового DataFrame с атрибутами, например allows_duplicate_labels, установленными в заданное значение

In [24]: df2 = df.set_flags(allows_duplicate_labels=True)

In [25]: df2.flags.allows_duplicate_labels
Out[25]: True

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

In [26]: df2.flags.allows_duplicate_labels = False

In [27]: df2.flags.allows_duplicate_labels
Out[27]: False

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

>>> raw = pd.read_csv("...")
>>> deduplicated = raw.groupby(level=0).first()  # remove duplicates
>>> deduplicated.flags.allows_duplicate_labels = False  # disallow going forward

Установка allows_duplicate_labels=False для Series или DataFrame с дублирующимися метками либо выполнение операции, приводящей к появлению дублирующихся меток в Series или DataFrame, где дубликаты запрещены, вызовет errors.DuplicateLabelError.

In [28]: df.rename(str.upper)
---------------------------------------------------------------------------
DuplicateLabelError                       Traceback (most recent call last)
Cell In[28], line 1
----> 1 df.rename(str.upper)

File ~/work/pandas/pandas/pandas/core/frame.py:5774, in DataFrame.rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
   5643 def rename(
   5644     self,
   5645     mapper: Renamer | None = None,
   (...)
   5653     errors: IgnoreRaise = "ignore",
   5654 ) -> DataFrame | None:
   5655     """
   5656     Rename columns or index labels.
   5657 
   (...)
   5772     4  3  6
   5773     """
-> 5774     return super()._rename(
   5775         mapper=mapper,
   5776         index=index,
   5777         columns=columns,
   5778         axis=axis,
   5779         copy=copy,
   5780         inplace=inplace,
   5781         level=level,
   5782         errors=errors,
   5783     )

File ~/work/pandas/pandas/pandas/core/generic.py:1140, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
   1138     return None
   1139 else:
-> 1140     return result.__finalize__(self, method="rename")

File ~/work/pandas/pandas/pandas/core/generic.py:6281, in NDFrame.__finalize__(self, other, method, **kwargs)
   6274 if other.attrs:
   6275     # We want attrs propagation to have minimal performance
   6276     # impact if attrs are not used; i.e. attrs is an empty dict.
   6277     # One could make the deepcopy unconditionally, but a deepcopy
   6278     # of an empty dict is 50x more expensive than the empty check.
   6279     self.attrs = deepcopy(other.attrs)
-> 6281 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
   6282 # For subclasses using _metadata.
   6283 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
     94 if not value:
     95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
     98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:716, in Index._maybe_check_unique(self)
    713 duplicates = self._format_duplicate_message()
    714 msg += f"\n{duplicates}"
--> 716 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
X        [0, 2]
Y        [1, 3]

Это сообщение об ошибке содержит дублирующиеся метки и числовые позиции всех дубликатов (включая «исходный» элемент) в Series или DataFrame

Сохранение запрета дублирующихся меток

Как правило, запрет дубликатов сохраняется при выполнении операций.

In [29]: s1 = pd.Series(0, index=["a", "b"]).set_flags(allows_duplicate_labels=False)

In [30]: s1
Out[30]: 
a    0
b    0
dtype: int64

In [31]: s1.head().rename({"a": "b"})
---------------------------------------------------------------------------
DuplicateLabelError                       Traceback (most recent call last)
Cell In[31], line 1
----> 1 s1.head().rename({"a": "b"})

File ~/work/pandas/pandas/pandas/core/series.py:5101, in Series.rename(self, index, axis, copy, inplace, level, errors)
   5094     axis = self._get_axis_number(axis)
   5096 if callable(index) or is_dict_like(index):
   5097     # error: Argument 1 to "_rename" of "NDFrame" has incompatible
   5098     # type "Union[Union[Mapping[Any, Hashable], Callable[[Any],
   5099     # Hashable]], Hashable, None]"; expected "Union[Mapping[Any,
   5100     # Hashable], Callable[[Any], Hashable], None]"
-> 5101     return super()._rename(
   5102         index,  # type: ignore[arg-type]
   5103         copy=copy,
   5104         inplace=inplace,
   5105         level=level,
   5106         errors=errors,
   5107     )
   5108 else:
   5109     return self._set_name(index, inplace=inplace, deep=copy)

File ~/work/pandas/pandas/pandas/core/generic.py:1140, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
   1138     return None
   1139 else:
-> 1140     return result.__finalize__(self, method="rename")

File ~/work/pandas/pandas/pandas/core/generic.py:6281, in NDFrame.__finalize__(self, other, method, **kwargs)
   6274 if other.attrs:
   6275     # We want attrs propagation to have minimal performance
   6276     # impact if attrs are not used; i.e. attrs is an empty dict.
   6277     # One could make the deepcopy unconditionally, but a deepcopy
   6278     # of an empty dict is 50x more expensive than the empty check.
   6279     self.attrs = deepcopy(other.attrs)
-> 6281 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
   6282 # For subclasses using _metadata.
   6283 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
     94 if not value:
     95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
     98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:716, in Index._maybe_check_unique(self)
    713 duplicates = self._format_duplicate_message()
    714 msg += f"\n{duplicates}"
--> 716 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
b        [0, 1]

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

Эта возможность является экспериментальной. В настоящее время многие методы не сохраняют значение allows_duplicate_labels. В будущих версиях ожидается, что каждый метод, принимающий или возвращающий один или несколько объектов DataFrame или Series, будет сохранять allows_duplicate_labels.

© 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/duplicates.html

Spec-Zone.ru

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