重複ラベル#
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:5144, in Series.reindex(self, index, axis, method, copy, level, fill_value, limit, tolerance)
5127 @doc(
5128 NDFrame.reindex, # type: ignore[has-type]
5129 klass=_shared_doc_kwargs["klass"],
(...)
5142 tolerance=None,
5143 ) -> Series:
-> 5144 return super().reindex(
5145 index=index,
5146 method=method,
5147 copy=copy,
5148 level=level,
5149 fill_value=fill_value,
5150 limit=limit,
5151 tolerance=tolerance,
5152 )
File ~/work/pandas/pandas/pandas/core/generic.py:5607, in NDFrame.reindex(self, labels, index, columns, axis, method, copy, level, fill_value, limit, tolerance)
5604 return self._reindex_multi(axes, copy, fill_value)
5606 # perform the reindex on the axes
-> 5607 return self._reindex_axes(
5608 axes, level, limit, tolerance, method, fill_value, copy
5609 ).__finalize__(self, method="reindex")
File ~/work/pandas/pandas/pandas/core/generic.py:5630, in NDFrame._reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
5627 continue
5629 ax = self._get_axis(a)
-> 5630 new_index, indexer = ax.reindex(
5631 labels, level=level, limit=limit, tolerance=tolerance, method=method
5632 )
5634 axis = self._get_axis_number(a)
5635 obj = obj._reindex_with_indexers(
5636 {axis: [new_index, indexer]},
5637 fill_value=fill_value,
5638 copy=copy,
5639 allow_dups=False,
5640 )
File ~/work/pandas/pandas/pandas/core/indexes/base.py:4429, in Index.reindex(self, target, method, level, limit, tolerance)
4426 raise ValueError("cannot handle a non-unique multi-index!")
4427 elif not self.is_unique:
4428 # GH#42568
-> 4429 raise ValueError("cannot reindex on an axis with duplicate labels")
4430 else:
4431 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:507, in NDFrame.set_flags(self, copy, allows_duplicate_labels)
505 df = self.copy(deep=copy and not using_copy_on_write())
506 if allows_duplicate_labels is not None:
--> 507 df.flags["allows_duplicate_labels"] = allows_duplicate_labels
508 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: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
の行ラベルと列ラベルの両方に適用されます。
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
またはDataFrame
でallows_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:5754, in DataFrame.rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
5623 def rename(
5624 self,
5625 mapper: Renamer | None = None,
(...)
5633 errors: IgnoreRaise = "ignore",
5634 ) -> DataFrame | None:
5635 """
5636 Rename columns or index labels.
5637
(...)
5752 4 3 6
5753 """
-> 5754 return super()._rename(
5755 mapper=mapper,
5756 index=index,
5757 columns=columns,
5758 axis=axis,
5759 copy=copy,
5760 inplace=inplace,
5761 level=level,
5762 errors=errors,
5763 )
File ~/work/pandas/pandas/pandas/core/generic.py:1139, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
1137 return None
1138 else:
-> 1139 return result.__finalize__(self, method="rename")
File ~/work/pandas/pandas/pandas/core/generic.py:6259, in NDFrame.__finalize__(self, other, method, **kwargs)
6252 if other.attrs:
6253 # We want attrs propagation to have minimal performance
6254 # impact if attrs are not used; i.e. attrs is an empty dict.
6255 # One could make the deepcopy unconditionally, but a deepcopy
6256 # of an empty dict is 50x more expensive than the empty check.
6257 self.attrs = deepcopy(other.attrs)
-> 6259 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
6260 # For subclasses using _metadata.
6261 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: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
内のすべての重複(「オリジナル」を含む)の数値位置が含まれています。
重複ラベルの伝播#
一般に、重複を許可しない設定は「固定」されています。操作を通じて保持されます。
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:5081, in Series.rename(self, index, axis, copy, inplace, level, errors)
5074 axis = self._get_axis_number(axis)
5076 if callable(index) or is_dict_like(index):
5077 # error: Argument 1 to "_rename" of "NDFrame" has incompatible
5078 # type "Union[Union[Mapping[Any, Hashable], Callable[[Any],
5079 # Hashable]], Hashable, None]"; expected "Union[Mapping[Any,
5080 # Hashable], Callable[[Any], Hashable], None]"
-> 5081 return super()._rename(
5082 index, # type: ignore[arg-type]
5083 copy=copy,
5084 inplace=inplace,
5085 level=level,
5086 errors=errors,
5087 )
5088 else:
5089 return self._set_name(index, inplace=inplace, deep=copy)
File ~/work/pandas/pandas/pandas/core/generic.py:1139, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
1137 return None
1138 else:
-> 1139 return result.__finalize__(self, method="rename")
File ~/work/pandas/pandas/pandas/core/generic.py:6259, in NDFrame.__finalize__(self, other, method, **kwargs)
6252 if other.attrs:
6253 # We want attrs propagation to have minimal performance
6254 # impact if attrs are not used; i.e. attrs is an empty dict.
6255 # One could make the deepcopy unconditionally, but a deepcopy
6256 # of an empty dict is 50x more expensive than the empty check.
6257 self.attrs = deepcopy(other.attrs)
-> 6259 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
6260 # For subclasses using _metadata.
6261 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: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]
警告
これは実験的な機能です。現在、多くのメソッドはallows_duplicate_labels
の値を伝播できません。将来のバージョンでは、1つ以上のDataFrameまたはSeriesオブジェクトを受け取るか返すすべてのメソッドがallows_duplicate_labels
を伝播することが期待されます。