本記事は、Pandas の公式ドキュメントのUser Guide - Duplicate Labelsを機械翻訳した後、一部の不自然な文章を手直ししたものである。
誤訳の指摘・代訳案・質問等があればコメント欄や編集リクエストでお願いします。
重複ラベル
Index
オブジェクトは一意である必要はなく、行や列のラベルが重複していてもかまいません。最初はこのことに少し戸惑うかもしれません。SQLに詳しい人なら、行ラベルはテーブルの主キーに似ていて、SQLテーブルでは決して重複を望まないことをご存じでしょう。しかし、pandasの役割の一つは、下流のシステムに行く前に、厄介な実世界のデータをきれいにすることです。そして実世界のデータには、たとえ一意であるはずのフィールドであっても重複があります。
このセクションでは、重複ラベルによって特定の操作の挙動がどのように変わるか、操作中に重複が発生しないようにする方法、または発生した場合にそれを検出する方法について説明します。
重複ラベルの影響
いくつかのpandasのメソッド(例えば Series.reindex()
)には、重複があると動作しないものがあります。出力が確定できないので、pandasはエラーを送出します。
s1 = pd.Series([0, 1, 2], index=["a", "b", "b"])
s1.reindex(["a", "b", "c"])
# ---------------------------------------------------------------------------
# ValueError Traceback (most recent call last)
# Input In [4], in <module>
# ----> 1 s1.reindex(["a", "b", "c"])
#
# File /pandas/pandas/core/series.py:4672, in Series.reindex(self, *args, **kwargs)
# 4668 raise TypeError(
# 4669 "'index' passed as both positional and keyword argument"
# 4670 )
# 4671 kwargs.update({"index": index})
# -> 4672 return super().reindex(**kwargs)
#
# File /pandas/pandas/core/generic.py:4974, in NDFrame.reindex(self, *args, **kwargs)
# 4971 return self._reindex_multi(axes, copy, fill_value)
# 4973 # perform the reindex on the axes
# -> 4974 return self._reindex_axes(
# 4975 axes, level, limit, tolerance, method, fill_value, copy
# 4976 ).__finalize__(self, method="reindex")
#
# File /pandas/pandas/core/generic.py:4994, in NDFrame._reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
# 4989 new_index, indexer = ax.reindex(
# 4990 labels, level=level, limit=limit, tolerance=tolerance, method=method
# 4991 )
# 4993 axis = self._get_axis_number(a)
# -> 4994 obj = obj._reindex_with_indexers(
# 4995 {axis: [new_index, indexer]},
# 4996 fill_value=fill_value,
# 4997 copy=copy,
# 4998 allow_dups=False,
# 4999 )
# 5000 # If we've made a copy once, no need to make another one
# 5001 copy = False
#
# File /pandas/pandas/core/generic.py:5040, in NDFrame._reindex_with_indexers(self, reindexers, fill_value, copy, allow_dups)
# 5037 indexer = ensure_platform_int(indexer)
# 5039 # TODO: speed up on homogeneous DataFrame objects (see _reindex_multi)
# -> 5040 new_data = new_data.reindex_indexer(
# 5041 index,
# 5042 indexer,
# 5043 axis=baxis,
# 5044 fill_value=fill_value,
# 5045 allow_dups=allow_dups,
# 5046 copy=copy,
# 5047 )
# 5048 # If we've made a copy once, no need to make another one
# 5049 copy = False
#
# File /pandas/pandas/core/internals/managers.py:679, in BaseBlockManager.reindex_indexer(self, new_axis, indexer, axis, fill_value, allow_dups, copy, consolidate, only_slice, use_na_proxy)
# 677 # some axes don't allow reindexing with dups
# 678 if not allow_dups:
# --> 679 self.axes[axis]._validate_can_reindex(indexer)
# 681 if axis >= self.ndim:
# 682 raise IndexError("Requested axis not found in manager")
#
# File /pandas/pandas/core/indexes/base.py:4107, in Index._validate_can_reindex(self, indexer)
# 4105 # trying to reindex on an axis with duplicates
# 4106 if not self._index_as_unique and len(indexer):
# -> 4107 raise ValueError("cannot reindex on an axis with duplicate labels")
#
# ValueError: cannot reindex on an axis with duplicate labels
その他のメソッド、例えばインデックス付けは、非常に驚くべき結果をもたらすことがあります。一般に、スカラーを使ったインデックスを作成すると、次元が下がります。DataFrame
をスカラーでスライスすると、Series
が返されます。Series
をスカラーでスライスすると、スカラーが返されます。しかし、重複ラベルがある場合はそうはなりません。
df1 = pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "A", "B"])
df1
# A A B
# 0 0 1 2
# 1 3 4 5
列に重複ラベルが存在しています。このとき、'B'
をスライスした場合は、シリーズが返却されます。
df1["B"] # シリーズ
# 0 2
# 1 5
# Name: B, dtype: int64
しかし、'A'
をスライスすると、データフレームが返却されます。
df1["A"] # データフレーム
# A A
# 0 0 1
# 1 3 4
この挙動は行ラベルに対しても適用されます。
df2 = pd.DataFrame({"A": [0, 1, 2]}, index=["a", "a", "b"])
df2
# A
# a 0
# a 1
# b 2
df2.loc["b", "A"] # スカラー
# 2
df2.loc["a", "A"] # シリーズ
# a 0
# a 1
# Name: A, dtype: int64
重複ラベルの検出
(行または列のラベルを格納する)Index
が一意であるかどうかは、Index.is_unique
で確認できます。
df2
# A
# a 0
# a 1
# b 2
df2.index.is_unique
# False
df2.columns.is_unique
# True
インデックスが一意であるかどうかのチェックは、大きなデータセットではやや高価です。pandasはこの結果をキャッシュするので、同じインデックスに対する再チェックは非常に高速です。
Index.duplicated()
は、ラベルが重複しているかどうかを示す真偽値のndarrayを返します。
df2.index.duplicated()
# array([False, True, False])
これは、重複する行を削除するためのブーリアンフィルタとして使用することができます。
df2.loc[~df2.index.duplicated(), :]
# A
# a 0
# b 2
重複ラベルを処理する際に、単にそのラベルを削除するだけではなく、追加のロジックが必要な場合は、インデックスにgroupby()
を使用するのが一般的なトリックです。たとえば、同じラベルを持つすべての行の平均をとって、重複を解決してみましょう。
df2.groupby(level=0).mean()
# A
# a 0.5
# b 2.0
重複ラベルを禁止する
バージョン1.2.0で追加
前述のように、重複の処理というのは生データを読み込む際に重要な機能です。とはいえ、(pandas.concat()
やrename()
などのメソッドからの)データ処理パイプラインの一部として重複を導入するのは避けたいかもしれません。Series
とDataFrame
は、.set_flags(ables_duplicate_labels=False)
を呼び出すことで、ラベルの重複を禁止します(デフォルトでは許可します)。重複するラベルがある場合は、例外が発生します。
pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)
# ---------------------------------------------------------------------------
# DuplicateLabelError Traceback (most recent call last)
# Input In [19], in <module>
# ----> 1 pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)
#
# File /pandas/pandas/core/generic.py:438, in NDFrame.set_flags(self, copy, allows_duplicate_labels)
# 436 df = self.copy(deep=copy)
# 437 if allows_duplicate_labels is not None:
# --> 438 df.flags["allows_duplicate_labels"] = allows_duplicate_labels
# 439 return df
#
# File /pandas/pandas/core/flags.py:105, in Flags.__setitem__(self, key, value)
# 103 if key not in self._keys:
# 104 raise ValueError(f"Unknown flag {key}. Must be one of {self._keys}")
# --> 105 setattr(self, key, value)
#
# File /pandas/pandas/core/flags.py:92, in Flags.allows_duplicate_labels(self, value)
# 90 if not value:
# 91 for ax in obj.axes:
# ---> 92 ax._maybe_check_unique()
# 94 self._allows_duplicate_labels = value
#
# File /pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
# 712 duplicates = self._format_duplicate_message()
# 713 msg += f"\n{duplicates}"
# --> 715 raise DuplicateLabelError(msg)
#
# DuplicateLabelError: Index has duplicates.
# positions
# label
# b [1, 2]
これはDataFrame
の行ラベルと列ラベルの両方に適用されます。
pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "B", "C"],).set_flags(
allows_duplicate_labels=False
)
# A B C
# 0 0 1 2
# 1 3 4 5
これは、そのオブジェクトが重複してラベルを持つことができるかどうかを示すallow_duplicate_labels
属性によって、チェックまたは設定することができます。
df = pd.DataFrame({"A": [0, 1, 2, 3]}, index=["x", "y", "X", "Y"]).set_flags(
allows_duplicate_labels=False
)
df
# A
# x 0
# y 1
# X 2
# Y 3
df.flags.allows_duplicate_labels
# False
DataFrame.set_flags()
は、allow_duplicate_labels
などの属性に値を設定した新しいDataFrame
を返すときに使用できます。
df2 = df.set_flags(allows_duplicate_labels=True)
df2.flags.allows_duplicate_labels
# True
この新しいDataFrame
は、古いDataFrame
と同じデータを参照するビューとして返されます。あるいは、同じオブジェクトに直接プロパティを設定することもできます。
df2.flags.allows_duplicate_labels = False
df2.flags.allows_duplicate_labels
# False
生の乱雑なデータを処理する場合、まず(ラベルが重複している可能性がある)乱雑なデータを読み込み、重複の削除を行い、その後、データパイプラインが重複を発生させないようにするのが良いでしょう。
raw = pd.read_csv("...")
deduplicated = raw.groupby(level=0).first() # 重複の削除
deduplicated.flags.allows_duplicate_labels = False # 後の重複を禁ずる
ラベルが重複しているSeries
やDataFrame
に対してallow_duplicate_labels=False
を設定しようとしたり、重複を禁止しているSeries
やDataFrame
に対して重複したラベルを導入する操作を行うと、errors.DuplicateLabelError
が発生します。
df.rename(str.upper)
# ---------------------------------------------------------------------------
# DuplicateLabelError Traceback (most recent call last)
# Input In [28], in <module>
# ----> 1 df.rename(str.upper)
#
# File /pandas/pandas/core/frame.py:5077, in DataFrame.rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
# 4958 def rename(
# 4959 self,
# 4960 mapper: Renamer | None = None,
# (...)
# 4968 errors: str = "ignore",
# 4969 ) -> DataFrame | None:
# 4970 """
# 4971 Alter axes labels.
# 4972
# (...)
# 5075 4 3 6
# 5076 """
# -> 5077 return super()._rename(
# 5078 mapper=mapper,
# 5079 index=index,
# 5080 columns=columns,
# 5081 axis=axis,
# 5082 copy=copy,
# 5083 inplace=inplace,
# 5084 level=level,
# 5085 errors=errors,
# 5086 )
#
# File /pandas/pandas/core/generic.py:1171, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
# 1169 return None
# 1170 else:
# -> 1171 return result.__finalize__(self, method="rename")
#
# File /pandas/pandas/core/generic.py:5549, in NDFrame.__finalize__(self, other, method, **kwargs)
# 5546 for name in other.attrs:
# 5547 self.attrs[name] = other.attrs[name]
# -> 5549 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
# 5550 # For subclasses using _metadata.
# 5551 for name in set(self._metadata) & set(other._metadata):
#
# File /pandas/pandas/core/flags.py:92, in Flags.allows_duplicate_labels(self, value)
# 90 if not value:
# 91 for ax in obj.axes:
# ---> 92 ax._maybe_check_unique()
# 94 self._allows_duplicate_labels = value
#
# File /pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
# 712 duplicates = self._format_duplicate_message()
# 713 msg += f"\n{duplicates}"
# --> 715 raise DuplicateLabelError(msg)
#
# DuplicateLabelError: Index has duplicates.
# positions
# label
# X [0, 2]
# Y [1, 3]
このエラーメッセージには、重複しているラベルと、Series
またはDataFrame
内の重複しているラベル(「オリジナル」を含む)の数値位置が含まれています。
重複ラベルの伝搬
一般に、重複を禁止することは「粘着性(sticky)」があり、操作によって保存されます。
s1 = pd.Series(0, index=["a", "b"]).set_flags(allows_duplicate_labels=False)
s1
# a 0
# b 0
# dtype: int64
s1.head().rename({"a": "b"})
# ---------------------------------------------------------------------------
# DuplicateLabelError Traceback (most recent call last)
# Input In [31], in <module>
# ----> 1 s1.head().rename({"a": "b"})
#
# File /pandas/pandas/core/series.py:4601, in Series.rename(self, index, axis, copy, inplace, level, errors)
# 4598 axis = self._get_axis_number(axis)
# 4600 if callable(index) or is_dict_like(index):
# -> 4601 return super()._rename(
# 4602 index, copy=copy, inplace=inplace, level=level, errors=errors
# 4603 )
# 4604 else:
# 4605 return self._set_name(index, inplace=inplace)
#
# File /pandas/pandas/core/generic.py:1171, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
# 1169 return None
# 1170 else:
# -> 1171 return result.__finalize__(self, method="rename")
#
# File /pandas/pandas/core/generic.py:5549, in NDFrame.__finalize__(self, other, method, **kwargs)
# 5546 for name in other.attrs:
# 5547 self.attrs[name] = other.attrs[name]
# -> 5549 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
# 5550 # For subclasses using _metadata.
# 5551 for name in set(self._metadata) & set(other._metadata):
#
# File /pandas/pandas/core/flags.py:92, in Flags.allows_duplicate_labels(self, value)
# 90 if not value:
# 91 for ax in obj.axes:
# ---> 92 ax._maybe_check_unique()
# 94 self._allows_duplicate_labels = value
#
# File /pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
# 712 duplicates = self._format_duplicate_message()
# 713 msg += f"\n{duplicates}"
# --> 715 raise DuplicateLabelError(msg)
#
# DuplicateLabelError: Index has duplicates.
# positions
# label
# b [0, 1]
これは試験的な機能です。現在、多くのメソッドがallow_duplicate_labels
の値の伝播に失敗しています。将来のバージョンでは、1つ以上のDataFrame
またはSeries
オブジェクトを取得したり返したりするすべてのメソッドがallows_duplicate_labels
を伝搬することが期待されています。