Pandas Drop Duplicates: Pythonで重複行を削除する方法
Updated on
重複行は、実世界のデータセットで最も一般的なデータ品質問題の1つです。繰り返しのAPIコール、重複するCSVエクスポート、故障したETLパイプライン、または手動データ入力時の単純なコピー&ペーストエラーによって忍び込みます。放置すると、重複は行数を膨らませ、平均値や合計値を歪め、機械学習モデルにバイアスを導入します。10,000件の顧客レコードがあるように見えるデータセットが、実際には8,200人のユニークな顧客と、その上に構築されたすべての計算を静かに破壊する1,800件のファントムエントリしか含んでいないかもしれません。
pandasのdrop_duplicates()メソッドは、これらの冗長な行を検出して削除する標準的な方法です。このガイドではすべてのパラメータを解説し、実世界での重複排除の一般的なパターンを示し、大規模データセットでのパフォーマンスに関する考慮事項を扱います。すべてのコード例はコピー可能で、期待される出力を含んでいます。
なぜ重複が重要なのか
コードに入る前に、重複行が正確に何を壊すのかを理解する価値があります:
| 問題 | 何が起こるか | 例 |
|---|---|---|
| 膨張したカウント | len(df)とvalue_counts()が過大にカウントする | 顧客が3回表示され、「総顧客数」が3倍高くなる |
| 誤った平均値 | mean()が重複行をより重く重み付けする | 高額注文が2回カウントされ、平均注文額が上方に歪む |
| 壊れたジョイン | 重複のあるキーでのマージが行の爆発を引き起こす | 多対多のマージが1:1マッピングではなくデカルト積を生成する |
| 不良なMLモデル | 繰り返しサンプルを含む訓練データがモデルにバイアスを与える | モデルが重複した例を記憶し、過学習する |
| ストレージの無駄 | 冗長な行がディスクとメモリを消費する | 2GBのデータセットが重複排除後に1.4GBになる可能性がある |
修正は簡単です:重複を見つけ、どのコピーを保持するか(もしあれば)を決定し、残りを削除します。
基本構文: df.drop_duplicates()
最も単純な呼び出しは、すべての列値が同一の行を削除します:
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob'],
'age': [30, 25, 30, 35, 25],
'city': ['NYC', 'LA', 'NYC', 'Chicago', 'LA']
})
print("Before:")
print(df)
df_clean = df.drop_duplicates()
print("\nAfter:")
print(df_clean)出力:
Before:
name age city
0 Alice 30 NYC
1 Bob 25 LA
2 Alice 30 NYC
3 Charlie 35 Chicago
4 Bob 25 LA
After:
name age city
0 Alice 30 NYC
1 Bob 25 LA
3 Charlie 35 Chicago行2と行4は行0と行1の完全なコピーだったため、削除されました。元のインデックス値(0, 1, 3)が保持されていることに注意してください。連続したインデックスが必要な場合は、.reset_index(drop=True)をチェーンしてください。
完全なメソッドシグネチャ
DataFrame.drop_duplicates(subset=None, keep='first', inplace=False, ignore_index=False)| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
subset | 列ラベルまたはリスト | None(全列) | 重複を特定する際にこれらの列のみを考慮する |
keep | 'first', 'last', False | 'first' | どの重複を保持するか;Falseはすべてのコピーを削除する |
inplace | bool | False | Trueの場合、DataFrameをインプレースで変更しNoneを返す |
ignore_index | bool | False | Trueの場合、結果のインデックスを0からn-1にリセットする |
まずdf.duplicated()で重複を見つける
重複を削除する前に、それらを検査したいことがよくあります。duplicated()メソッドは重複行をマークするブーリアンSeriesを返します:
df = pd.DataFrame({
'order_id': [101, 102, 103, 101, 104, 102],
'product': ['Widget', 'Gadget', 'Widget', 'Widget', 'Gizmo', 'Gadget'],
'amount': [29.99, 49.99, 29.99, 29.99, 19.99, 49.99]
})
# Show which rows are duplicates
print(df.duplicated())出力:
0 False
1 False
2 False
3 True
4 False
5 True
dtype: bool実際の重複行を表示するには:
duplicates = df[df.duplicated(keep=False)]
print(duplicates)出力:
order_id product amount
0 101 Widget 29.99
1 102 Gadget 49.99
3 101 Widget 29.99
5 102 Gadget 49.99keep=Falseを使用すると、すべてのコピーを重複としてマークします(2番目の出現だけでなく)。これにより、重複に関与するすべての行を確認できます。
重複のカウント
重複行がいくつ存在するかを素早く数えるには:
num_duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {num_duplicates}")
total_rows = len(df)
unique_rows = df.drop_duplicates().shape[0]
print(f"Total: {total_rows}, Unique: {unique_rows}, Duplicates: {total_rows - unique_rows}")出力:
Number of duplicate rows: 2
Total: 6, Unique: 4, Duplicates: 2これはクリーニングの前後で有用なサニティチェックです。
subsetパラメータ: 特定の列のみをチェック
すべての列が一致することを要求するのではなく、キー列に基づいて重複排除したいことがよくあります。subsetパラメータを使用すると、一意性を決定する列を指定できます:
df = pd.DataFrame({
'email': ['alice@mail.com', 'bob@mail.com', 'alice@mail.com', 'charlie@mail.com'],
'name': ['Alice', 'Bob', 'Alice Smith', 'Charlie'],
'signup_date': ['2025-01-01', '2025-01-02', '2025-01-15', '2025-01-03']
})
print("Original:")
print(df)
# Deduplicate by email only
df_deduped = df.drop_duplicates(subset=['email'])
print("\nDeduplicated by email:")
print(df_deduped)出力:
Original:
email name signup_date
0 alice@mail.com Alice 2025-01-01
1 bob@mail.com Bob 2025-01-02
2 alice@mail.com Alice Smith 2025-01-15
3 charlie@mail.com Charlie 2025-01-03
Deduplicated by email:
email name signup_date
0 alice@mail.com Alice 2025-01-01
1 bob@mail.com Bob 2025-01-02
3 charlie@mail.com Charlie 2025-01-03nameとsignup_dateの値が異なっていても、alice@mail.comが既に行0に存在していたため、行2が削除されました。複数の列を渡すこともできます:subset=['email', 'name']は両方の列が一致する場合にのみ行を重複とみなします。
keepパラメータ: first、last、またはFalse
keepパラメータは、どの出現を残すかを制御します:
df = pd.DataFrame({
'sensor_id': ['S1', 'S2', 'S1', 'S2', 'S1'],
'reading': [22.5, 18.3, 23.1, 18.3, 24.0],
'timestamp': ['08:00', '08:00', '09:00', '09:00', '10:00']
})
print("keep='first' (default):")
print(df.drop_duplicates(subset=['sensor_id'], keep='first'))
print("\nkeep='last':")
print(df.drop_duplicates(subset=['sensor_id'], keep='last'))
print("\nkeep=False (drop all duplicates):")
print(df.drop_duplicates(subset=['sensor_id'], keep=False))出力:
keep='first' (default):
sensor_id reading timestamp
0 S1 22.5 08:00
1 S2 18.3 08:00
keep='last':
sensor_id reading timestamp
3 S2 18.3 09:00
4 S1 24.0 10:00
keep=False (drop all duplicates):
Empty DataFrame
Columns: [sensor_id, reading, timestamp]
Index: []| keepの値 | 動作 | ユースケース |
|---|---|---|
'first' | 最初の出現を保持し、後のものを削除する | 最も古いレコードを保持する |
'last' | 最後の出現を保持し、前のものを削除する | 最新のレコードを保持する |
False | 重複がある行をすべて削除する | 本当にユニークな行を見つける(コピーがまったくない) |
上の例では、両方のセンサーIDが複数回出現するため、keep=Falseは空のDataFrameを返します。
インプレース vs 新しいDataFrameを返す
デフォルトでは、drop_duplicates()は新しいDataFrameを返し、元のDataFrameは変更されません。inplace=Trueを設定すると、元のDataFrameを直接変更します:
df = pd.DataFrame({
'id': [1, 2, 2, 3],
'value': ['a', 'b', 'b', 'c']
})
# Returns new DataFrame (original unchanged)
df_new = df.drop_duplicates()
print(f"Original length: {len(df)}, New length: {len(df_new)}")
# Modifies in place (returns None)
df.drop_duplicates(inplace=True)
print(f"After inplace: {len(df)}")出力:
Original length: 4, New length: 3
After inplace: 3モダンなpandasスタイルでは、inplace=Trueを避け、代わりに代入を使用することを推奨しています(df = df.drop_duplicates())。これにより、特にチェーン操作で、コードが読みやすくデバッグしやすくなります。
大文字小文字を区別しない重複検出
デフォルトでは、pandasは"Alice"と"alice"を異なる値として扱います。大文字小文字を区別しない重複排除を行うには、まず列を正規化します:
df = pd.DataFrame({
'name': ['Alice', 'alice', 'ALICE', 'Bob', 'bob'],
'score': [90, 85, 92, 78, 80]
})
# Create a normalized column for comparison
df['name_lower'] = df['name'].str.lower()
# Deduplicate on the normalized column, keep the first original-case entry
df_deduped = df.drop_duplicates(subset=['name_lower']).drop(columns=['name_lower'])
print(df_deduped)出力:
name score
0 Alice 90
3 Bob 78このパターンは、大文字小文字を区別しない重複を正しく識別しながら、元の大文字小文字を保持します。
実世界の例: 顧客データベースのクリーニング
現実的なシナリオをご紹介します。CRMから顧客エクスポートを受け取りましたが、同じ顧客が異なる営業担当者によって複数回入力されていました:
import pandas as pd
customers = pd.DataFrame({
'customer_id': [1001, 1002, 1001, 1003, 1002, 1004],
'name': ['Acme Corp', 'Beta Inc', 'Acme Corp', 'Gamma LLC', 'Beta Inc.', 'Delta Co'],
'email': ['acme@mail.com', 'beta@mail.com', 'acme@mail.com', 'gamma@mail.com', 'beta@mail.com', 'delta@mail.com'],
'revenue': [50000, 30000, 52000, 45000, 30000, 20000],
'last_contact': ['2025-12-01', '2025-11-15', '2026-01-10', '2025-10-20', '2025-11-15', '2026-01-05']
})
print(f"Rows before cleaning: {len(customers)}")
print(f"Duplicate customer_ids: {customers.duplicated(subset=['customer_id']).sum()}")
# Sort by last_contact descending so the most recent entry is first
customers['last_contact'] = pd.to_datetime(customers['last_contact'])
customers = customers.sort_values('last_contact', ascending=False)
# Keep the most recent record for each customer_id
customers_clean = customers.drop_duplicates(subset=['customer_id'], keep='first')
customers_clean = customers_clean.sort_values('customer_id').reset_index(drop=True)
print(f"\nRows after cleaning: {len(customers_clean)}")
print(customers_clean)出力:
Rows before cleaning: 6
Duplicate customer_ids: 2
Rows after cleaning: 4
customer_id name email revenue last_contact
0 1001 Acme Corp acme@mail.com 52000 2026-01-10
1 1002 Beta Inc beta@mail.com 30000 2025-11-15
2 1003 Gamma LLC gamma@mail.com 45000 2025-10-20
3 1004 Delta Co delta@mail.com 20000 2026-01-05ここでのキーパターンはまずソートし、次にkeep='first'で重複排除することです。重複排除の前にlast_contactを降順でソートすることで、各顧客の最新レコードが残ります。
実世界の例: Webスクレイピング結果の重複排除
Webスクレイパーは、ページが複数回クロールされたり、ページネーションが重複したりすると、よく重複を生成します:
import pandas as pd
scraped = pd.DataFrame({
'url': [
'https://shop.com/item/101',
'https://shop.com/item/102',
'https://shop.com/item/101',
'https://shop.com/item/103',
'https://shop.com/item/102',
'https://shop.com/item/104',
],
'title': ['Blue Widget', 'Red Gadget', 'Blue Widget', 'Green Gizmo', 'Red Gadget', 'Yellow Thing'],
'price': [19.99, 29.99, 19.99, 39.99, 31.99, 14.99],
'scraped_at': ['2026-02-01', '2026-02-01', '2026-02-02', '2026-02-02', '2026-02-02', '2026-02-02']
})
print(f"Total scraped rows: {len(scraped)}")
print(f"Unique URLs: {scraped['url'].nunique()}")
# Keep the latest scrape for each URL (prices may have changed)
scraped['scraped_at'] = pd.to_datetime(scraped['scraped_at'])
scraped = scraped.sort_values('scraped_at', ascending=False)
products = scraped.drop_duplicates(subset=['url'], keep='first').reset_index(drop=True)
print(f"\nCleaned rows: {len(products)}")
print(products)出力:
Total scraped rows: 6
Unique URLs: 4
Cleaned rows: 4
url title price scraped_at
0 https://shop.com/item/101 Blue Widget 19.99 2026-02-02
1 https://shop.com/item/102 Red Gadget 31.99 2026-02-02
2 https://shop.com/item/103 Green Gizmo 39.99 2026-02-02
3 https://shop.com/item/104 Yellow Thing 14.99 2026-02-02Red Gadgetの価格がスクレイピング間で29.99から31.99に更新されたことに注意してください。最新のエントリを保持することで、最新の価格を取得しています。
drop_duplicates() vs groupby().first(): いつどちらを使うか
どちらのアプローチもデータを重複排除できますが、動作が異なります:
import pandas as pd
df = pd.DataFrame({
'user_id': [1, 1, 2, 2, 3],
'action': ['login', 'purchase', 'login', 'login', 'purchase'],
'timestamp': ['2026-01-01', '2026-01-02', '2026-01-01', '2026-01-03', '2026-01-01']
})
# Method 1: drop_duplicates
result1 = df.drop_duplicates(subset=['user_id'], keep='first')
print("drop_duplicates:")
print(result1)
# Method 2: groupby().first()
result2 = df.groupby('user_id').first().reset_index()
print("\ngroupby().first():")
print(result2)出力:
drop_duplicates:
user_id action timestamp
0 1 login 2026-01-01
2 2 login 2026-01-01
4 3 purchase 2026-01-01
groupby().first():
user_id action timestamp
0 1 login 2026-01-01
1 2 login 2026-01-01
2 3 purchase 2026-01-01結果は似ていますが、重要な違いがあります:
| 特徴 | drop_duplicates() | groupby().first() |
|---|---|---|
| 速度 | 単純な重複排除ではより高速 | グループ化のオーバーヘッドにより低速 |
| NaN処理 | NaN値をそのまま保持する | first()はデフォルトでNaNをスキップする |
| インデックス | 元のインデックスを保持する | グループキーにリセットされる |
| 集約 | 他の列を集約できない | agg()と組み合わせて複数列のサマリーが可能 |
| メモリ | メモリ使用量が少ない | 中間のGroupByオブジェクトを作成する |
| ユースケース | 完全または部分的な重複を削除する | 集約を計算しながら重複排除する |
経験則: 単純に重複行を削除したい場合はdrop_duplicates()を使用してください。重複行の値も集約する必要がある場合はgroupby().first()(またはgroupby().agg())を使用してください -- 例えば、最初の名前を保持しながら重複顧客レコード間で売上を合計する場合などです。
大規模DataFrameのパフォーマンスのヒント
数百万行を扱う場合、重複排除がボトルネックになることがあります。以下は高速化するための実践的な方法です:
1. subset列を指定する
すべての列をチェックするよりも、キー列のみをチェックする方が高速です:
# Slower: checks every column
df.drop_duplicates()
# Faster: checks only the key column
df.drop_duplicates(subset=['user_id'])2. 適切なデータ型を使用する
カーディナリティが低い場合、重複排除の前に文字列列をcategory型に変換します:
df['status'] = df['status'].astype('category')
df['country'] = df['country'].astype('category')
df_clean = df.drop_duplicates(subset=['status', 'country'])3. 必要な場合にのみ重複排除前にソートする
keep='first'がどの行を選択するかを制御するためだけに大規模DataFrameをソートすると、かなりの時間がかかります。どの重複が残るかを気にしない場合は、ソートをスキップしてください。
4. 非常に大きなファイルにはチャンク処理を検討する
メモリに収まらないファイルの場合、チャンクで処理します:
chunks = pd.read_csv('large_file.csv', chunksize=100_000)
seen = set()
clean_chunks = []
for chunk in chunks:
chunk = chunk.drop_duplicates(subset=['id'])
new_rows = chunk[~chunk['id'].isin(seen)]
seen.update(new_rows['id'].tolist())
clean_chunks.append(new_rows)
df_clean = pd.concat(clean_chunks, ignore_index=True)5. ベンチマーク: 典型的なパフォーマンス
| 行数 | チェックする列 | 時間(約) |
|---|---|---|
| 100K | すべて(10列) | ~15 ms |
| 1M | すべて(10列) | ~150 ms |
| 1M | 1列 | ~50 ms |
| 10M | 1列 | ~500 ms |
これらの数値はハードウェアやデータ型によって異なりますが、重要なポイントはdrop_duplicates()が線形にスケールし、ほとんどのデータセットを1秒未満で処理することです。
PyGWalkerでクリーニングしたデータを探索する
重複を削除した後、次のステップは通常、クリーニングしたデータセットを探索することです -- 分布を確認し、外れ値を見つけ、重複排除が期待通りに機能したことを検証します。複数のmatplotlibやseaborn呼び出しを書く代わりに、PyGWalker (opens in a new tab)を使用できます。これは、pandas DataFrameをJupyter Notebook上で直接インタラクティブなTableau風のビジュアライゼーションインターフェースに変換するオープンソースのPythonライブラリです。
import pandas as pd
import pygwalker as pyg
# Your cleaned customer data
customers_clean = pd.DataFrame({
'customer_id': [1001, 1002, 1003, 1004],
'name': ['Acme Corp', 'Beta Inc', 'Gamma LLC', 'Delta Co'],
'revenue': [52000, 30000, 45000, 20000],
'region': ['East', 'West', 'East', 'South']
})
# Launch interactive visualization
walker = pyg.walk(customers_clean)PyGWalkerを使えば、regionをx軸に、revenueをy軸にドラッグして、地域別の売上分布を即座に確認できます。フィールドをドラッグ&ドロップするだけで、棒グラフ、散布図、ヒストグラム、ヒートマップを作成できます -- チャートコードは不要です。重複排除後にクリーニングロジックが妥当な結果を生み出したかを確認したい場合に特に便利です。
pip install pygwalkerでPyGWalkerをインストールするか、Google Colab (opens in a new tab)で試してみてください。
FAQ
drop_duplicates()は元のDataFrameを変更しますか?
いいえ、デフォルトではdrop_duplicates()は新しいDataFrameを返し、元のDataFrameは変更されません。インプレースで変更するにはinplace=Trueを渡しますが、推奨されるアプローチは代入を使用することです:df = df.drop_duplicates()。
1つの列のみに基づいて重複を削除するにはどうすればよいですか?
列名をsubsetパラメータに渡します:df.drop_duplicates(subset=['email'])。これは各ユニークなメールアドレスの最初の行を保持し、他の列の違いに関係なく、同じメールアドレスを持つ後続の行を削除します。
duplicated()とdrop_duplicates()の違いは何ですか?
duplicated()はどの行が重複かをマークするブーリアンSeriesを返します(検査とカウントに便利)。drop_duplicates()は重複行が削除されたDataFrameを返します。データを理解するためにまずduplicated()を使用し、次にクリーニングするためにdrop_duplicates()を使用します。
条件に基づいて重複を削除できますか?
drop_duplicates()で直接はできません。代わりに、まず条件列でDataFrameをソートし、次にkeep='first'でdrop_duplicates()を呼び出します。例えば、各顧客の最高売上の行を保持するには:df.sort_values('revenue', ascending=False).drop_duplicates(subset=['customer_id'], keep='first')。
大文字小文字を区別しない重複をどう処理しますか?
一時的な小文字列を作成し、それで重複排除を行い、ヘルパー列を削除します:df['key'] = df['name'].str.lower()の後にdf.drop_duplicates(subset=['key']).drop(columns=['key'])。これにより、元の大文字小文字を保持しながら、重複を正しく識別できます。
まとめ
重複行の削除は、あらゆるデータクリーニングワークフローにおける基本的なステップです。pandasのdrop_duplicates()メソッドは、わずかなパラメータで重複排除タスクの大部分を処理します:
- **
subset**を使用して、すべての列ではなく特定の列で重複排除します。 - **
keep='first'またはkeep='last'を使用して、どの出現が残るかを制御します;keep=False**ですべてのコピーを削除します。 - **
duplicated()**を使用して、削除する前に重複を検査しカウントします。 - 特定の行を保持する必要がある場合(例:最新のレコード)は、重複排除の前にソートします。
- 大文字小文字を区別しない重複排除には、まず一時列で小文字に正規化します。
- 大規模データセットでは、
subset列を制限し、適切なデータ型を使用してパフォーマンスを維持します。
データがクリーニングされたら、PyGWalker (opens in a new tab)のようなツールを使えば、チャートコードを書かずに結果を視覚的に探索でき、重複排除が正しく機能したことを確認してすぐに分析に移ることができます。