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