Дублирующиеся метки
Объекты 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