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

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:5172, in Series.reindex(self, index, axis, method, copy, level, fill_value, limit, tolerance)
   5155 @doc(
   5156     NDFrame.reindex,  # type: ignore[has-type]
   5157     klass=_shared_doc_kwargs["klass"],
   (...)
   5170     tolerance=None,
   5171 ) -> Series:
-> 5172     return super().reindex(
   5173         index=index,
   5174         method=method,
   5175         copy=copy,
   5176         level=level,
   5177         fill_value=fill_value,
   5178         limit=limit,
   5179         tolerance=tolerance,
   5180     )

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

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

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:511, in NDFrame.set_flags(self, copy, allows_duplicate_labels)
    509 df = self.copy(deep=copy and not using_copy_on_write())
    510 if allows_duplicate_labels is not None:
--> 511     df.flags["allows_duplicate_labels"] = allows_duplicate_labels
    512 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:5789, in DataFrame.rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
   5658 def rename(
   5659     self,
   5660     mapper: Renamer | None = None,
   (...)
   5668     errors: IgnoreRaise = "ignore",
   5669 ) -> DataFrame | None:
   5670     """
   5671     Rename columns or index labels.
   5672 
   (...)
   5787     4  3  6
   5788     """
-> 5789     return super()._rename(
   5790         mapper=mapper,
   5791         index=index,
   5792         columns=columns,
   5793         axis=axis,
   5794         copy=copy,
   5795         inplace=inplace,
   5796         level=level,
   5797         errors=errors,
   5798     )

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

File ~/work/pandas/pandas/pandas/core/generic.py:6284, in NDFrame.__finalize__(self, other, method, **kwargs)
   6277 if other.attrs:
   6278     # We want attrs propagation to have minimal performance
   6279     # impact if attrs are not used; i.e. attrs is an empty dict.
   6280     # One could make the deepcopy unconditionally, but a deepcopy
   6281     # of an empty dict is 50x more expensive than the empty check.
   6282     self.attrs = deepcopy(other.attrs)
-> 6284 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
   6285 # For subclasses using _metadata.
   6286 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:5109, in Series.rename(self, index, axis, copy, inplace, level, errors)
   5102     axis = self._get_axis_number(axis)
   5104 if callable(index) or is_dict_like(index):
   5105     # error: Argument 1 to "_rename" of "NDFrame" has incompatible
   5106     # type "Union[Union[Mapping[Any, Hashable], Callable[[Any],
   5107     # Hashable]], Hashable, None]"; expected "Union[Mapping[Any,
   5108     # Hashable], Callable[[Any], Hashable], None]"
-> 5109     return super()._rename(
   5110         index,  # type: ignore[arg-type]
   5111         copy=copy,
   5112         inplace=inplace,
   5113         level=level,
   5114         errors=errors,
   5115     )
   5116 else:
   5117     return self._set_name(index, inplace=inplace, deep=copy)

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

File ~/work/pandas/pandas/pandas/core/generic.py:6284, in NDFrame.__finalize__(self, other, method, **kwargs)
   6277 if other.attrs:
   6278     # We want attrs propagation to have minimal performance
   6279     # impact if attrs are not used; i.e. attrs is an empty dict.
   6280     # One could make the deepcopy unconditionally, but a deepcopy
   6281     # of an empty dict is 50x more expensive than the empty check.
   6282     self.attrs = deepcopy(other.attrs)
-> 6284 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
   6285 # For subclasses using _metadata.
   6286 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.