Python Pandas入門:重複データを見つけて削除する duplicated()とdrop_duplicates()

こんにちは。前回は、Pandas DataFrameにおける欠損値への対処法として、削除(.dropna())と補完(.fillna())について学びました。データのお掃除スキルがまた一つ上がりましたね。

さて、データクリーニングの旅はまだまだ続きます。欠損値と並んで、データ分析の前処理でよく遭遇し、対処が必要となるのが「重複データ」です。つまり、全く同じ情報が複数行にわたって記録されてしまっている状態です。

アンケートの二重回答、システムのバグによるデータ二重登録、データ統合時のミスなど、重複データが発生する原因は様々です。この重複データを放置しておくと、

  • データの件数を誤って多く数えてしまう
  • 平均値などの集計結果が不正確になる
  • 機械学習モデルが特定のデータパターンを過剰に学習してしまう

といった問題を引き起こす可能性があります。そのため、分析を始める前に重複データを見つけ出し、適切に処理することが重要になります。

今回の記事では、Pandasを使ってこの重複データに対処する方法を学びます。具体的には、以下の内容を扱います。

  • 重複データとは何か、なぜ問題になるのかを再確認する。
  • どの行が重複しているのかを確認する.duplicated()メソッドの使い方。
  • 重複している行を削除する.drop_duplicates()メソッドの使い方。
  • 特定の列だけを見て重複を判断する方法(subset引数)。
  • 重複している行のうち、どれを残すか(最初か、最後か、全て削除か)を指定する方法(keep引数)。

今回も具体的なサンプルデータを使って、コードの実行結果を見ながら、重複データの確認と削除のステップを一つずつ丁寧に解説していきます。データのお掃除、第3弾、頑張りましょう!

重複データとは? なぜ問題になる?

重複データとは、文字通り、DataFrameの中に全く同じ内容の行が複数存在している状態を指します。全ての列の値が完全に一致している行が2つ以上ある場合、それらは重複しているとみなされます。(ただし、後述するように、特定の列だけを見て重複を判断することも可能です。)

重複データが発生する原因としては、以下のようなものが考えられます。

  • ユーザーの誤操作: Webフォームからの申し込みを誤って2回送信してしまった、など。
  • システムの不具合: データ連携処理で同じデータが複数回送られてしまった、データベースへの書き込み処理が重複して実行された、など。
  • データ収集方法の問題: 複数のソースからデータを集めた際に、同じデータが両方のソースに含まれていた、など。
  • 意図的な重複(?): まれに、意図的に同じデータを複数回記録するケースもありますが、通常は上記のような意図しない原因が多いです。

では、なぜ重複データを処理する必要があるのでしょうか?主な理由は以下の通りです。

  • 不正確な集計: 例えば、ユニークユーザー数を数えたいのに重複データがあると、実際の数よりも多くカウントしてしまいます。平均購入額などを計算する際も、同じユーザーのデータが複数回含まれると結果が歪みます。
  • 分析結果の偏り: 特定のデータが重複して多く存在することで、分析結果がそのデータに引っ張られてしまう可能性があります。
  • 機械学習モデルへの悪影響: 重複データが多いと、モデルがその特定のデータパターンだけを強く学習してしまい、未知のデータに対する予測性能(汎化性能)が低下することがあります。また、学習時間が無駄に長くなることもあります。
  • リソースの無駄: 単純に、同じデータを複数保持することは、ストレージ容量やメモリの無駄遣いになります。

これらの理由から、データ分析や機械学習を行う前には、重複データの有無を確認し、必要であれば削除などの対処を行うのが一般的です。

サンプルデータの準備(重複行を含む)

重複データの処理方法を学ぶために、意図的に重複行を含んだサンプルDataFrameを作成しましょう。今回は、簡単な商品購入履歴のようなデータを想定します。

import pandas as pd

# 重複行を含むサンプルデータ
data_duplicates = {
    '注文ID': [101, 102, 103, 101, 104, 105, 103], # 101と103が重複
    '顧客名': ['佐藤', '鈴木', '高橋', '佐藤', '田中', '伊藤', '高橋'],
    '商品': ['商品A', '商品B', '商品C', '商品A', '商品B', '商品A', '商品C'],
    '価格': [1000, 1500, 2000, 1000, 1500, 1000, 2000]
}

df_dup = pd.DataFrame(data_duplicates)

# 作成したDataFrameを表示
print("--- 重複を含むDataFrame ---")
print(df_dup)

実行結果:

--- 重複を含むDataFrame ---
   注文ID 顧客名   商品    価格
0    101   佐藤  商品A  1000
1    102   鈴木  商品B  1500
2    103   高橋  商品C  2000
3    101   佐藤  商品A  1000  <-- インデックス0と全く同じ
4    104   田中  商品B  1500
5    105   伊藤  商品A  1000
6    103   高橋  商品C  2000  <-- インデックス2と全く同じ

このDataFrameを見ると、

  • インデックス3の行は、インデックス0の行と全ての列の値が完全に一致しています。
  • インデックス6の行は、インデックス2の行と全ての列の値が完全に一致しています。

このように、2組の重複行が存在するデータが準備できました。このdf_dupを使って、重複の確認と削除を行っていきましょう。

重複行の確認:.duplicated()メソッド

まず、どの行が重複しているのかを確認する方法です。これには.duplicated()メソッドを使います。このメソッドは、各行がそれよりも前に出現した行と重複しているかどうかを判定し、結果をブール値(True/False)のSeriesとして返します。

基本的な使い方

引数を指定せずに.duplicated()を実行してみましょう。

# 各行が重複しているかを確認
duplicates_check = df_dup.duplicated()
print(duplicates_check)

実行結果:

0    False
1    False
2    False
3     True
4    False
5    False
6     True
dtype: bool

結果はブール値のSeriesです。各インデックスに対応する値の意味は以下の通りです。

  • False: その行は、それより前には同じ内容の行が出現していない。
  • True: その行は、それより前に全く同じ内容の行が出現している(つまり、この行は重複である)。

実行結果を見ると、インデックス36Trueになっています。これは、

  • インデックス3の行は、それより前に同じ内容の行(インデックス0)が存在するため、重複 (True) と判定された。
  • インデックス6の行は、それより前に同じ内容の行(インデックス2)が存在するため、重複 (True) と判定された。

ことを意味します。一方、インデックス02の行は、それらが最初に出現した行なので、重複とはみなされずFalseになっています。これが.duplicated()のデフォルトの挙動です。

どの重複行を残すか:keep引数

.duplicated()のデフォルトの挙動(最初に出現した行はFalse、それ以降の重複行をTrueとする)は、keep='first'が指定された場合と同じです。

keep引数を使うと、どの行を重複とみなすか(Trueにするか)を制御できます。

  • keep='first' (デフォルト): 最初に出現した行以外の重複行をTrueとする。
  • keep='last': 最後に出現した行以外の重複行をTrueとする。
  • keep=False: 全ての重複行(最初に出現したものも含む)をTrueとする。

それぞれの挙動を見てみましょう。

keep='last' の場合:

# 最後に出現した行以外を重複とみなす
duplicates_keep_last = df_dup.duplicated(keep='last')
print(duplicates_keep_last)

実行結果:

0     True
1    False
2     True
3    False
4    False
5    False
6    False
dtype: bool

今度は、インデックス02Trueになりました。これは、

  • インデックス0の行は、それより後(インデックス3)に同じ内容の行が存在するため、重複 (True) と判定された。
  • インデックス2の行は、それより後(インデックス6)に同じ内容の行が存在するため、重複 (True) と判定された。

ことを意味します。最後に出現したインデックス36の行はFalseになっています。

keep=False の場合:

# 全ての重複行を重複とみなす
duplicates_keep_false = df_dup.duplicated(keep=False)
print(duplicates_keep_false)

実行結果:

0     True
1    False
2     True
3     True
4    False
5    False
6     True
dtype: bool

インデックス0, 2, 3, 6が全てTrueになりました。これは、これらの行が(自身を含めて)同じ内容の行が複数存在するためです。完全にユニークな行(インデックス1, 4, 5)だけがFalseになっています。

特定の列だけで重複を判断:subset引数

デフォルトでは、全ての列の値が一致する場合に重複とみなしますが、「特定の列の値だけが一致していれば重複とみなす」という場合もあります。例えば、「同じ顧客が同じ商品を複数回注文しているか調べたい(注文IDや価格は違っても良い)」といった場合です。

これにはsubset引数を使います。subsetには、重複を判断する基準となる列名のリストを指定します。

例:「顧客名」と「商品」の組み合わせが重複しているかを確認

# '顧客名'と'商品'の組み合わせで重複を確認 (最初に出現したもの以外をTrue)
duplicates_subset = df_dup.duplicated(subset=['顧客名', '商品'], keep='first')
print(duplicates_subset)

実行結果:

0    False  # ('佐藤', '商品A') 初出
1    False  # ('鈴木', '商品B') 初出
2    False  # ('高橋', '商品C') 初出
3     True   # ('佐藤', '商品A') 2回目
4    False  # ('田中', '商品B') 初出
5    False  # ('伊藤', '商品A') 初出
6     True   # ('高橋', '商品C') 2回目
dtype: bool

インデックス3(顧客名=’佐藤’, 商品=’商品A’)とインデックス6(顧客名=’高橋’, 商品=’商品C’)がTrueになりました。これは、それぞれの組み合わせが前に出現しているためです。他の列(注文ID、価格)の値は無視されます。

重複行の数を数える

DataFrame内に重複行がいくつあるかを知りたい場合は、.duplicated()の結果(ブール値のSeries)に対して.sum()メソッドを適用します。True1False0として扱われるため、Trueの数(=重複行の数)が合計されます。

# 重複行の数を数える(デフォルト: keep='first')
num_duplicates = df_dup.duplicated().sum()
print(f"重複行の数 (最初を除く): {num_duplicates}")

実行結果:

重複行の数 (最初を除く): 2

最初に出現したものを除くと、重複行は2つあることが分かりました。

重複行の削除:.drop_duplicates()メソッド

重複している行を確認できたら、次はその重複行を削除します。これには.drop_duplicates()メソッドを使います。

.drop_duplicates()は、.duplicated()と考え方が似ており、どの行を重複とみなして削除するかをkeep引数で、どの列を基準に重複を判断するかをsubset引数で指定できます。

基本的な使い方(デフォルト: 最初の行を残す)

引数を指定せずに.drop_duplicates()を実行すると、デフォルト(keep='first')の挙動となり、重複している行のうち、最初に出現した行だけを残し、それ以降の重複行を削除します。

# 重複行を削除(デフォルト: 最初の行を残す)
df_dropped = df_dup.drop_duplicates()
print("--- 重複行削除後 (keep='first') ---")
print(df_dropped)

実行結果:

--- 重複行削除後 (keep='first') ---
   注文ID 顧客名   商品    価格
0    101   佐藤  商品A  1000
1    102   鈴木  商品B  1500
2    103   高橋  商品C  2000
4    104   田中  商品B  1500
5    105   伊藤  商品A  1000

元のDataFrameから、重複していたインデックス36の行が削除され、ユニークな行だけが残りました。インデックス02は最初に出現した行なので残っています。

どの重複行を残すか指定:keep引数

keep引数を使うことで、残す行を指定できます。

  • keep='first' (デフォルト): 最初に出現した行を残す。
  • keep='last': 最後に出現した行を残す。
  • keep=False: 重複している行を全て削除する(ユニークな行だけを残す)。

keep='last' の場合:

# 重複行のうち、最後の行を残す
df_dropped_last = df_dup.drop_duplicates(keep='last')
print("--- 重複行削除後 (keep='last') ---")
print(df_dropped_last)

実行結果:

--- 重複行削除後 (keep='last') ---
   注文ID 顧客名   商品    価格
1    102   鈴木  商品B  1500
3    101   佐藤  商品A  1000  <-- インデックス0の代わりにこちらが残る
4    104   田中  商品B  1500
5    105   伊藤  商品A  1000
6    103   高橋  商品C  2000  <-- インデックス2の代わりにこちらが残る

今度は、最初に出現したインデックス02の行が削除され、最後に出現したインデックス36の行が残りました。

keep=False の場合:

# 重複している行を全て削除
df_dropped_all_duplicates = df_dup.drop_duplicates(keep=False)
print("--- 重複行削除後 (keep=False) ---")
print(df_dropped_all_duplicates)

実行結果:

--- 重複行削除後 (keep=False) ---
   注文ID 顧客名   商品    価格
1    102   鈴木  商品B  1500
4    104   田中  商品B  1500
5    105   伊藤  商品A  1000

重複していた組(インデックス0と3、インデックス2と6)が全て削除され、完全にユニークな行だけが残りました。

特定の列だけで重複を判断して削除:subset引数

.duplicated()と同様に、subset引数を使って、特定の列の値が一致する場合に重複とみなし、削除することができます。

例:「顧客名」と「商品」の組み合わせが重複している場合、最初の行を残して削除

# '顧客名'と'商品'の組み合わせで重複を判断し、最初の行を残す
df_dropped_subset = df_dup.drop_duplicates(subset=['顧客名', '商品'], keep='first')
print("--- subset=['顧客名', '商品'] で重複削除後 (keep='first') ---")
print(df_dropped_subset)

実行結果:

--- subset=['顧客名', '商品'] で重複削除後 (keep='first') ---
   注文ID 顧客名   商品    価格
0    101   佐藤  商品A  1000
1    102   鈴木  商品B  1500
2    103   高橋  商品C  2000
4    104   田中  商品B  1500
5    105   伊藤  商品A  1000

subsetを指定せずに全列で重複削除した場合と同じ結果になりましたが、これは重複の判断基準が「顧客名」と「商品」の組み合わせだけで行われている点が異なります。

.drop_duplicates()の注意点:inplace=True

.dropna()と同様に、.drop_duplicates()もデフォルトでは元のDataFrameを変更せず、処理結果を反映した新しいDataFrameを返します。元のDataFrameを直接変更したい場合は、inplace=True引数を指定します(ただし、使用には注意が必要です)。

# inplace=True を使って元のDataFrameを直接変更する例(実行注意)
# df_copy = df_dup.copy()
# print("--- inplace=True 実行前 ---")
# print(df_copy)
# df_copy.drop_duplicates(inplace=True)
# print("--- inplace=True 実行後 ---")
# print(df_copy)

重複処理の際の考えどころ

重複データ処理は単純に見えますが、実際に適用する際にはいくつか考えるべき点があります。

  • 何を「重複」とみなすか?: 全ての列が一致する場合のみか、それとも特定のキーとなる列(例:顧客ID、注文IDなど)が一致すれば重複とみなすか? subset引数を適切に設定する必要があります。
  • どの行を残すか?: 重複があった場合、最初に見つかった行、最後に見つかった行、あるいは全て削除するか? keep引数の選択が重要です。例えば、最新の情報を残したい場合はkeep='last'、データ入力の最初の記録を残したい場合はkeep='first'、重複自体がエラーである場合はkeep=Falseで全て削除して原因を調査する、といった使い分けが考えられます。
  • 本当に削除して良いのか?: 一見重複に見えても、実は意味のあるデータである可能性はないか? 例えば、同じ顧客が同じ商品を複数回購入した場合、それは正当な購買履歴かもしれません。単純にdrop_duplicates()を適用する前に、なぜ重複が発生しているのか、その重複がデータとして妥当なのかを検討することが重要です。

機械的に重複削除を行うのではなく、データの意味を考えながら適切な処理を選択することが、質の高い分析につながります。

まとめ

今回は、Pandas DataFrameにおける重複データの確認と削除の方法について学びました。

  • 重複データの問題点: 集計ミスや分析結果の偏り、モデル性能低下などを引き起こす可能性があるため、対処が必要。
  • 重複行の確認 (.duplicated()):
    • 各行がそれ以前の行と重複しているかをTrue/Falseで返す。
    • keep='first'(デフォルト), 'last', False でどの行を重複とみなすか制御できる。
    • subset=[列名リスト] で特定の列だけで重複を判断できる。
    • .duplicated().sum() で重複行数をカウントできる。
  • 重複行の削除 (.drop_duplicates()):
    • 重複行を削除した新しいDataFrameを返す(デフォルトでは元のDataFrameは変更されない)。
    • keep引数で残す行(最初、最後、全て削除)を指定できる。
    • subset引数で重複判断の基準となる列を指定できる。
    • inplace=True で元のDataFrameを直接変更できる(注意が必要)。
  • 重複処理の考えどころ: 何を重複とみなし、どの行を残すか、本当に削除して良いデータかを慎重に検討することが重要。

欠損値処理と重複データ処理は、データクリーニングにおける二大基本作業と言えます。これらのスキルを身につけることで、より信頼性の高いデータ分析や機械学習モデル構築への道が開けます。

次回予告

データのお掃除(欠損値処理、重複処理)が一段落しました。データが少しきれいになったところで、次はいよいよデータの「中身」をより深く理解するためのステップに進みましょう。

次回は、データをグループ化して集計する「グループ集計」について解説します。例えば、「部署ごとに平均年齢や評価スコアを集計したい」「商品ごとに販売数をカウントしたい」といった、データを特定のカテゴリでまとめて分析する強力なテクニックです。Pandasの.groupby()メソッドの使い方をマスターしましょう!お楽しみに!