重複するラベル#

Index オブジェクトは一意である必要はありません。行または列のラベルが重複していても構いません。最初は少し戸惑うかもしれません。SQLに詳しい方なら、行ラベルはテーブルの主キーに似ており、SQLテーブルに重複があってはならないことをご存知でしょう。しかし、pandasの役割の1つは、乱雑な現実世界のデータを下流システムに送る前にクリーンアップすることです。そして、現実世界のデータには、一意であるべきフィールドにも重複が含まれています。

このセクションでは、重複するラベルが特定の操作の動作をどのように変更するか、操作中に重複が発生するのを防ぐ方法、または重複を検出する方法について説明します。

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()などのメソッドから)重複を導入することを避けたい場合があるでしょう。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: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()を使用すると、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

重複ラベルを持つSeriesまたはDataFrameallows_duplicate_labels=Falseを設定したり、重複を禁止する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の値を伝播できません。将来のバージョンでは、1つ以上のDataFrameまたはSeriesオブジェクトを受け取る、または返すすべてのメソッドがallows_duplicate_labelsを伝播することが期待されています。