重复标签#

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:5525, in Series.reindex(self, index, axis, method, copy, level, fill_value, limit, tolerance)
   5299 def reindex(  # type: ignore[override]
   5300     self,
   5301     index=None,
   (...)   5309     tolerance=None,
   5310 ) -> Series:
   5311     """
   5312     Conform Series to new index with optional filling logic.
   5313 
   (...)   5523     See the :ref:`user guide <basics.reindexing>` for more.
   5524     """
-> 5525     return super().reindex(
   5526         index=index,
   5527         method=method,
   5528         level=level,
   5529         fill_value=fill_value,
   5530         limit=limit,
   5531         tolerance=tolerance,
   5532         copy=copy,
   5533     )

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

File ~/work/pandas/pandas/pandas/core/generic.py:5498, in NDFrame._reindex_axes(self, axes, level, limit, tolerance, method, fill_value)
   5495     continue
   5497 ax = self._get_axis(a)
-> 5498 new_index, indexer = ax.reindex(
   5499     labels, level=level, limit=limit, tolerance=tolerance, method=method
   5500 )
   5502 axis = self._get_axis_number(a)
   5503 obj = obj._reindex_with_indexers(
   5504     {axis: [new_index, indexer]},
   5505     fill_value=fill_value,
   5506     allow_dups=False,
   5507 )

File ~/work/pandas/pandas/pandas/core/indexes/base.py:4253, in Index.reindex(self, target, method, level, limit, tolerance)
   4250     raise ValueError("cannot handle a non-unique multi-index!")
   4251 elif not self.is_unique:
   4252     # GH#42568
-> 4253     raise ValueError("cannot reindex on an axis with duplicate labels")
   4254 else:
   4255     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]: np.int64(2)

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

重复标签检测#

您可以使用 Index.is_unique 检查 Index(存储行或列标签)是否唯一。

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

禁止重复标签#

如上所述,处理重复项是读取原始数据时的重要功能。尽管如此,您可能希望避免在数据处理管道中引入重复项(例如,通过 pandas.concat()rename() 等方法)。SeriesDataFrame禁止重复标签,方法是调用 .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:469, in NDFrame.set_flags(self, copy, allows_duplicate_labels)
    467 df = self.copy(deep=False)
    468 if allows_duplicate_labels is not None:
--> 469     df.flags["allows_duplicate_labels"] = allows_duplicate_labels
    470 return df

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

File ~/work/pandas/pandas/pandas/core/flags.py:108, in Flags.allows_duplicate_labels(self, value)
    106 if not value:
    107     for ax in obj.axes:
--> 108         ax._maybe_check_unique()
    110 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:721, in Index._maybe_check_unique(self)
    718 duplicates = self._format_duplicate_message()
    719 msg += f"\n{duplicates}"
--> 721 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() 可用于返回一个具有诸如 allows_duplicate_labels 等属性设置为某个值的新 DataFrame

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

在具有重复标签的 SeriesDataFrame 上设置 allows_duplicate_labels=False,或对禁止重复项的 SeriesDataFrame 执行引入重复项的操作,将引发 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:6471, in DataFrame.rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
   6352 """
   6353 Rename columns or index labels.
   6354 
   (...)   6468 4  3  6
   6469 """
   6470 self._check_copy_deprecation(copy)
-> 6471 return super()._rename(
   6472     mapper=mapper,
   6473     index=index,
   6474     columns=columns,
   6475     axis=axis,
   6476     inplace=inplace,
   6477     level=level,
   6478     errors=errors,
   6479 )

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

File ~/work/pandas/pandas/pandas/core/generic.py:6167, in NDFrame.__finalize__(self, other, method, **kwargs)
   6161 if other.attrs:
   6162     # We want attrs propagation to have minimal performance
   6163     # impact if attrs are not used; i.e. attrs is an empty dict.
   6164     # One could make the deepcopy unconditionally, but a deepcopy
   6165     # of an empty dict is 50x more expensive than the empty check.
   6166     self.attrs = deepcopy(other.attrs)
-> 6167 self.flags.allows_duplicate_labels = (
   6168     self.flags.allows_duplicate_labels
   6169     and other.flags.allows_duplicate_labels
   6170 )
   6171 # For subclasses using _metadata.
   6172 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:108, in Flags.allows_duplicate_labels(self, value)
    106 if not value:
    107     for ax in obj.axes:
--> 108         ax._maybe_check_unique()
    110 self._allows_duplicate_labels = value

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

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

此错误消息包含重复的标签,以及 SeriesDataFrame 中所有重复项(包括“原始”)的数字位置。

重复标签传播#

通常,禁止重复项是“粘性”的。它会在操作过程中被保留。

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:5231, in Series.rename(self, index, axis, copy, inplace, level, errors)
   5224     axis = self._get_axis_number(axis)
   5226 if callable(index) or is_dict_like(index):
   5227     # error: Argument 1 to "_rename" of "NDFrame" has incompatible
   5228     # type "Union[Union[Mapping[Any, Hashable], Callable[[Any],
   5229     # Hashable]], Hashable, None]"; expected "Union[Mapping[Any,
   5230     # Hashable], Callable[[Any], Hashable], None]"
-> 5231     return super()._rename(
   5232         index,  # type: ignore[arg-type]
   5233         inplace=inplace,
   5234         level=level,
   5235         errors=errors,
   5236     )
   5237 else:
   5238     return self._set_name(index, inplace=inplace)

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

File ~/work/pandas/pandas/pandas/core/generic.py:6167, in NDFrame.__finalize__(self, other, method, **kwargs)
   6161 if other.attrs:
   6162     # We want attrs propagation to have minimal performance
   6163     # impact if attrs are not used; i.e. attrs is an empty dict.
   6164     # One could make the deepcopy unconditionally, but a deepcopy
   6165     # of an empty dict is 50x more expensive than the empty check.
   6166     self.attrs = deepcopy(other.attrs)
-> 6167 self.flags.allows_duplicate_labels = (
   6168     self.flags.allows_duplicate_labels
   6169     and other.flags.allows_duplicate_labels
   6170 )
   6171 # For subclasses using _metadata.
   6172 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:108, in Flags.allows_duplicate_labels(self, value)
    106 if not value:
    107     for ax in obj.axes:
--> 108         ax._maybe_check_unique()
    110 self._allows_duplicate_labels = value

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

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

警告

这是一项实验性功能。目前,许多方法未能传播 allows_duplicate_labels 值。预计在未来的版本中,所有接受或返回一个或多个 DataFrame 或 Series 对象的函数都将传播 allows_duplicate_labels